Linux文件操作:从C接口到系统调用
目录
一:文件
1.1狭义文件理解
1.2广义理解
1.3文件操作的归类认知
1.4系统角度
1.5前置知识预备
二:回顾C文件接口
2.1打开文件
2.2写文件
2.3读文件
2.4输出信息到显示器
2.5stdin&stdout&stderr
2.6打开文件的方式
三:系统文件I/O
3.1⼀种传递标志位的方法
3.2创建文件
3.3写文件
3.4读文件
3.5接口介绍
3.6 open函数返回值
3.7文件描述符fd
3.7.1 0&1&2
3.7.2文件描述符的分配规则
3.7.3重定向
3.7.4使用dup2系统调用
3.7.5在myshell中加入重定向
一:文件
1.1狭义文件理解
文件在磁盘里
磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
磁盘是外设(即是输出设备也是输入设备)
磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO
1.2广义理解
Linux下一切皆文件(键盘、显示器、网卡、磁盘……这些都是抽象化的过程)(后面会讲如何去 理解)
1.3文件操作的归类认知
对于0KB的空文件是占用磁盘空间的
文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)
所有的文件操作本质是文件内容操作和文件属性操作
1.4系统角度
对文件的操作本质是进程对文件的操作
磁盘的管理者是操作系统
文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的
1.5前置知识预备
结论:我们研究文件,实际上是在研究文件和进程之间的关系
文件:
1.打开的文件 --- 内存
2.没打开的文件 --- 磁盘(大多数)
两者构成文件系统
二:回顾C文件接口
2.1打开文件
nt main()
{FILE* fp = fopen("myfile","w");if(!fp){printf("fopen error!\n");}while(1);fclose(fp);return 0;
}
打开的myfile文件在哪个路径下?
在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢? 可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息:
其中:
cwd:指向当前进程运行目录的⼀个符号链接。
exe:指向启动当前进程的可执行文件(完整路径)的符号链接。打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS 就能知道要创建的文件放在哪里。
2.2写文件
int main()
{FILE* fp = fopen("myfile","w");if(!fp){printf("fopen error!\n");}const char *mesage = "hello wolrd\n";int cont = 5;while(cont--)fwrite(mesage,strlen(mesage),1,fp);fclose(fp);return 0;
}
2.3读文件
int main()
{FILE *fp = fopen("myfile", "r");if (!fp){printf("fopen error!\n");return 1;}char buf[1024];const char *mesage = "hello wolrd\n";//读文件while (1){// 注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明size_t s = fread(buf, 1, strlen(mesage), fp);if (s > 0){buf[s] = 0;printf("%s", buf);}if (feof(fp)){break;}}fclose(fp);return 0;
}
可以简单修改上面代码实现cat指令
int main(int argc,char* argv[])
{if(argc != 2){printf("argc error!\n");return 1;}FILE *fp = fopen(argv[1], "r");if (!fp){printf("fopen error!\n");return 2;}char buf[1024];while (1){// 注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明size_t s = fread(buf, 1, strlen(buf), fp);if (s > 0){buf[s] = 0;printf("%s", buf);}if (feof(fp)){break;}}fclose(fp);return 0;
}
2.4输出信息到显示器
int main()
{const char *msage = "hello Linux\n";fwrite(msage,strlen(msage),1,stdout);printf("hello Linux\n");fputs("aaaaaa\n",stdout);// 内容 基本单元 长度 打印位置fwrite("bbbb\n",1,5,stdout);fprintf(stdout,"hehehe\n");return 0;
}
2.5stdin&stdout&stderr
C默认会打开三个输入输出流,分别是stdin(标准输入),stdout(标准输出),stderr(标准错误)
仔细观察发现,这三个流的类型都是FILE*,fopen返回值类型,文件指针
2.6打开文件的方式
r Open text file for reading.
The stream is positioned at the beginning of the file.
r+ Open for reading and writing.The stream is positioned at the beginning of the file.
将文章内容清空 创建一个文件
w Truncate(缩短) file to zero length or create text file for writing.The stream is positioned at the beginning of the file.
O_WRONLY | O_CREAT| O_TRUNC
w+ Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file).追加
The file is created if it does not exist.
The stream is positioned at the end of the file.
O_WRONLY | O_CREAT O_APPEND
a+ Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.
我们所用的C语言接口,底层一定是封装的对应的文件类系统调用,文章下面详细介绍
三:系统文件I/O
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:
3.1⼀种传递标志位的方法
使用位图的好处:
#define ONE (1 << 0) // 0;
#define TWO (1 << 1) // 2;
#define THREE (1 << 2) // 4;
#define FOUR (1 << 3) // 8;
#define FIVE (1 << 4) // 16;void PrintTest(int flags)
{if (flags & ONE){// 此处可添加对应的功能printf("one\n");}if (flags & TWO){// 此处可添加对应的功能printf("two\n");}if (flags & THREE){// 此处可添加对应的功能printf("three\n");}if (flags & FOUR){// 此处可添加对应的功能printf("four\n");}if (flags & FIVE){// 此处可添加对应的功能printf("five\n");}
}int main()
{// 通过传不同的位置标记,实现不同的功能PrintTest(ONE);printf("################\n");PrintTest(ONE | TWO);printf("################\n");PrintTest(TWO | THREE | FIVE);
}
操作文件,除了面的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来 进行文件访问,先来直接以系统代码的形式,实现和上面一模一样的代码。
而我们下面讲的系统调用的传参方式,就是使用的传不同的标志位实现不同的功能
3.2创建文件
int main()
{// 创建普通用户,只写打开//umask默认是0002open("log1.txt",O_WRONLY|O_CREAT,0666);//更改umask权限umask(0);open("log2.txt",O_WRONLY|O_CREAT,0666);return 0;
}
由上述代码可以看出,每一个进程都要自己的umask,如果我们不设置umask会默认从系统中继承,系统的umask是0002
修改上述代码,可以实现简单的touch 命令
int main(int argc,char* argv[])
{if(argc != 2){printf("argc 错误\n");return 1;}open(argv[1],O_WRONLY|O_CREAT,0666);return 0;
}
3.3写文件
int main()
{umask(0);// 不存在就创建 存在清空 追加int fd = open("log.txt",O_WRONLY | O_CREAT| O_TRUNC | O_APPEND,0666);if(fd < 0){perror("open");return 1;}const char* mesage = "hello Linux1!\n";int cont = 5;while(cont--)write(fd,mesage,strlen(mesage));//fd: 文件描述符,后⾯讲, msg:缓冲区⾸地址, len: 本次读取,期望写⼊多少个字节的数据。 返回值:实际写了多少字节数据 }
3.4读文件
//读文件
int main()
{int fd = open("log.txt",O_RDONLY);if(fd < 0){perror("open");return 1;}const char* msage = "hello Linux!\n";char buffer[1024];while(1){ssize_t s = read(fd,buffer,strlen(msage));//类比writeif(s > 0){printf("%s",buffer);}else{break;}}close(fd);//关闭文件}
3.5接口介绍
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>//flags:32bit位,每一个bit位都当做一个参数,位图
文件存在: int open(const char *pathname, int flags);
// mode:文件权限
文件不存在 :int open(const char *pathname, int flags, mode_t mode);pathname: 要打开或创建的⽬标⽂件
flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏“或”运算,构成flags参数:本质上是bit位只有一个位置为1的宏O_RDONLY: 只读打开O_WRONLY: 只写打开,文件不存在打开失败O_RDWR : 读,写打开这三个常量,必须指定⼀个且只能指定⼀个O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限O_APPEND: 追加写O_TRUNC:文件存在就清空文件返回值:成功:新打开的⽂件描述符失败:-1
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
fopen中的w相当于:
open("log.txt",O_WRONLY | O_CREAT|O_TRUNC,0666)
a相当于:
open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666)
write read close lseek ,类比C文件相关接口。
3.6 open函数返回值
在认识返回值之前,先来认识⼀下两个概念:系统调用和库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。
而open close read write lseek 都属于系统提供的接口,称之为系统调用接
回忆一在下操作系统概念的文章的一张图:
文章链接:冯诺依曼体系与操作系统核心-CSDN博客
系统调用接口和库函数的关系,一目了然。 所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。
在C语言中,字符串以\0为结尾
但是在系统文件中,不是这样认为的,会把\0(不可显字符)当做乱码处理
3.7文件描述符fd
通过对open函数的认识,我们知道了文件描述符就是一个小整数
3.7.1 0&1&2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
int main()
{char buf[1024];ssize_t s = read(0,buf,sizeof(buf));if(s > 0){write(1,buf,strlen(buf));write(2,buf,strlen(buf));}return 0;
}
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示⼀个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向⼀张表files_struct,该表 最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
通过前面的验证和使用,我们已经知道fd是数组下标,是在系统层面访问文件的唯一方式
那什么是FILE?什么是FILE*?
FILE是C语言提供的,类似struct FILE(结构体/类),里面一定封装了fd。
验证:
int main()
{printf("stdin : %d \n",stdin->_fileno);printf("stdout : %d \n",stdout->_fileno);printf("stderr : %d \n",stderr->_fileno);FILE* fp = fopen("log.txt","w");printf("fp : %d \n",fp->_fileno);return 0;
}
对于以上原理结论我们可通过内核源码验证: 首先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/sched.h (3.10.0-1160.71.1.el7.x86_64是内核版 本,可使用uname -a 自行查看服务器配置,因为这个⽂件夹只有⼀个,所以也不⽤刻意去分辨, 内核版本其实也随意)
要查看内容可直接⽤vscode在windows下打开内核源代码
相关结构体所在位置
struct task_struct : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/sched.hstruct files_struct : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/fdtable.hstruct file : /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/fs.h
3.7.2文件描述符的分配规则
int main()
{close(0);int fd1 = open("log1.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd2 = open("log2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd3 = open("log3.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd4 = open("log4.txt",O_WRONLY|O_CREAT|O_APPEND,0666);printf("fd1 : %d\n",fd1);printf("fd2 : %d\n",fd2);printf("fd3 : %d\n",fd3);printf("fd4 : %d\n",fd4);fflush(stdout);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
发现是结果是:fd1: 0 ,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
3.7.3重定向
int main()
{close(1);int fd1 = open("log1.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd2 = open("log2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd3 = open("log3.txt",O_WRONLY|O_CREAT|O_APPEND,0666);int fd4 = open("log4.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//系统层面fd=1指向log1.txt但是printf不知道,它的底层逻辑就是从1中输出printf("fd1 : %d\n",fd1);printf("fd2 : %d\n",fd2);printf("fd3 : %d\n",fd3);printf("fd4 : %d\n",fd4);fflush(stdout);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log1.txt当中,其中,fd=1。这 种现象叫做输出重定向。常见的重定向有: > ,>> ,< 那重定向的本质是什么呢?
3.7.4使用dup2系统调用
#include <unistd.h>int dup2(int oldfd, int newfd);
//dup2(fd,1) 把fd拷贝到1中,oldfd可以看作活得时间更长
int main()
{int fd = open("./log.txt",O_CREAT|O_RDWR);if(fd < 0){perror("open");return 1;}close(1);dup2(fd,1);//把fd的*file拷贝到1中while(1){char buf[1024];ssize_t s = read(0,buf,sizeof(buf)-1);if(s < 0){perror("read");return 2;}printf("%s",buf);fflush(stdout);}return 0;
}
printf是C库当中的IO函数,一般往stdout中输出,但是底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
3.7.5在myshell中加入重定向
文件描述符的生命周期随进程,进程退出,文件描述符生命周期结束
需要增添改变的代码块:
//与重定向有关的全局变量
#define NoneRedir 0
#define InputRedir 1
#define OutPutRedir 2
#define AppRedir 3int redir = NoneRedir;
char* filename = nullptr;//删除多余的空格
//"> log.txt"
#define TrimSpace(pos) do{\while(isspace(*pos)){\pos++;\}\
}while(0)//do while是一层外壳,用于保护内部函数void RestCommandLine()
{//清空全局变量数据memset(gargv,0,sizeof(gargv));gargc = 0;//重定向redir = NoneRedir;filename = nullptr;
}void ParseRedir(char command_buffer[],int len)
{//分析是哪种重定向int end = len-1;while(end >= 0){//"ls -a -l" < file.txtif(command_buffer[end] == '<'){redir = InputRedir;command_buffer[end] = 0;filename = &command_buffer[end]+1;TrimSpace(filename);break;}else if(command_buffer[end] == '>'){//"ls -a -l" >> file.txtif(command_buffer[end-1] == '>'){redir = AppRedir;command_buffer[end] = 0;command_buffer[end-1] = 0;filename = &command_buffer[end]+1;TrimSpace(filename);break;}else{redir = OutPutRedir;command_buffer[end] = 0;filename = &command_buffer[end]+1;TrimSpace(filename);break;}}else{end--;}}
}void DoRedir()
{//1.重定向工作让子进程做//2.程序替换不会影响重定向if(redir == InputRedir){if(filename){int fd = open(filename, O_RDONLY);if (fd < 0){exit(5);}dup2(fd, 0);}else{exit(4);}}else if(redir == OutPutRedir){if(filename){int fd = open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);if(fd < 0){exit(7);}dup2(fd,1);}else{exit(6);}}else if(redir == AppRedir){if(filename){int fd = open(filename,O_CREAT| O_WRONLY|O_APPEND,0666);if(fd < 0){exit(9);}dup2(fd,1);}else{exit(8);}}else{//没有重定向,什么都不做}}// 4.执行命令
bool ExecuteCommand()
{// 让子进程执行pid_t id = fork();if (id < 0)return false;if (id == 0){// 子进程DoRedir();// 1.执行命令execvpe(gargv[0], gargv, genv);// 2.退出exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0); // 子进程工作时,父进程阻塞等待子进程if (rid > 0){if (WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}return true;}return false;
}// 将"ls -l -a -n"转换为"ls" "-l" "-a" "-n"
void ParseCommand(char command_buffer[])
{//"ls -a -l -n"const char *sep = " ";// strtok首次使用传数组和分割变量,之后再使用,传空指针和分割字符串gargv[gargc++] = strtok(command_buffer, sep);// 分割成功返回字符串地址,失败返回nullptr,返回nullptr时while判断为false,跳出while循环while ((bool)(gargv[gargc++] = strtok(nullptr, sep)));gargc--; // 删掉最后返回的nullptr
}// 3.分析用户命令
void ParseCommandLine(char command_buffer[],int len)
{RestCommandLine();ParseRedir(command_buffer,len);ParseCommand(command_buffer);
}