文章目录
- 基础IO
- 系统相关接口
- 文件描述符
- 一切皆文件
- 文件描述符的分配规则
- 重定向
- fork后的文件描述符
基础IO
系统相关接口
在C语言中对文件的操作有fopen打开、fclose关闭、fread读、fwrite写等函数;其实这些都是在系统调用接口上进行的封装。
这里介绍4个系统调用接口:
头文件 | #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unsitd.h> |
---|---|
打开 | int open( const char * pathname, int flags); |
打开 | int open( const char * pathname,int flags, mode_t mode); |
关闭 | int close(int fd); |
读 | ssize_t read(int fd, void *buf, size_t count); |
写 | ssize_t write(int fd, const void *buf, size_t count); |
-
open:打开一个文件
参数介绍:
参数 功能 pathname 文件的路径,如 “./text.txt” flags O_RDONLY是宏定义,代表一个比特位,这里的这个是只读打开。对应C语言中"W"的打开方式 mode 当创建新文件时才使用第三个参数,他是给文件设置权限,一般设置为0644(8进制) - O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 读、写打开
- O_APPEND 每次写时都加到文件的尾端
- O_CREAT 若此文件不存在则创建它(这时需要用第二个函数添加权限)
- O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。
- O_TRUNC 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0
- O_NOCTTY 如果pathname指的是终端设备,则不将此设备分配作为此进程的控制终端。
- O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I / O操作设置非阻塞方式。
- O_SYNC 使每次write都等到物理I / O操作完成,即数据已经写到硬盘才返回。没有O_SYNC选项,则把数据写到缓冲区就返回,缓冲区会在合适的时候把缓冲区的数据一起写到硬盘,这样会减少操作硬盘的次数。
返回值:
- 返回最小的空闲的文件描述符(文件描述符在下面解释,这里先介绍函数用法),如果失败则返回-1并设置errno
void test1() { FILE* fd1 = fopen("./test.txt","w"); //c语言是把权限和打开方式已经封装好了 if(fd1 == NULL) { perror("fopen fail"); return 1; } int fd2 = open("./test.txt",O_WRONLY | O_CREAT,0644); if(fd2 < 0) { perror("open"); return 1; } }
O_WRONLY | O_CREAT其实是两个比特位相或,每个比特位定义成一个宏并对应一种打开方式,这里是只写打开,若此文件不存在则创建它
-
close:关闭一个文件(描述符)
参数介绍:
- fd代表一个文件描述符,文件描述符具体在后面解释
返回值
- 成功返回0,失败返回-1且设置errno
void test1() { FILE* fd1 = fopen("./test.txt","w"); //c语言是把权限和打开方式已经封装好了 if(fd1 == NULL) { perror("fopen fail"); return 1; } fclose(fd1); int fd2 = open("./test.txt",O_WRONLY | O_CREAT,0644); if(fd2 < 0) { perror("open"); return 1; } close(fd2); }
-
read:向以打开的文件读取数据
参数介绍:
- fd为文件描述符;
- buf表示读出数据缓冲区地址;
- count表示读出的字节数。
返回值:
-
若读取成功,则返回读到的字节数;
-
若失败,返回-1;
-
若已达到文件尾,则返回0
char put[64]; ssize_t ret = read(0,put,sizeof(put)); put[ret - 1] = 0;//清除\n,在63位置 printf("%s\n",put);
这里的0也是文件描述符代表键盘 stdin,也就是从键盘读取到put数组中,这里读取的字节数是64(最大值),但读到末尾就自动结束了
ret接收到读取的有效字节数,因为\n也会被读取,且在最后一位,所以把最后一个字符设置成0在打印时就不会出现换行了
-
write:向以打开的文件读写数据
参数介绍:
- fd为文件描述符;
- buf指定的缓冲区,即指针,指向一段内存单元;
- count表示写入的字节数。
返回值:
- 成功 :返回写入的字节数
- 失败 :-1
char put[64]; ssize_t ret = read(0,put,sizeof(put)); put[ret - 1] = 0;//清除\n printf("%s\n",put); const char* str = "hello\n"; write(1,str,strlen(str));
1是文件描述符代表显示器,这里就是从put里向屏幕写入(输出),写入的字节数是有效字符长度(strlen不包括\0)
文件描述符
文件描述符是一个非负整数,本质上是一个索引值
我们知道文件一般是存储在磁盘当中的,那么试问创建一个空文件不写内容它占存储么?
这里虽然显示的是0,但是其实他是占空间的,不然怎么证明它存在,(这里的0只是根据文件内容显示的)。
而实际上在我们创建文件时,会分为两个部分:属性+内容
属性记录了这个文件的创建时间、位置等等的信息。属性信息等等都可看成是一种数据,既然是数据就必然占用空间
而我们查看的大小、占用空间,实际上是记录的内容占用大小,是属性数据记录的
所以当我们创建一个文件时由:内容+属性两部分组成,而属性信息是只要在创建那一刻的同时也就创建了
在内核中打开文件的也有着对应的结构体:struct file{ 文件属性 }; 也就是打开时操作系统把文件的属性信息描述成一个结构体。
有了这个文件属性信息结构体就可以找到文件内容
我们知道程序运行起来后,会变成进程,一个进程可以打开多个文件;那打开多个文件 OS就必然要对这些文件进行管理,管理离不开“先描述,再组织”。
那打开多个文件,就会有多个struct file,操作系统为了方便管理,会把它们组织成数据结构,例如打开5个文件,代码如下:
void test3() { int fd1 = open("./test.txt",O_WRONLY | O_CREAT,0644); int fd2 = open("./test.txt",O_WRONLY | O_CREAT,0644); int fd3 = open("./test.txt",O_WRONLY | O_CREAT,0644); int fd4 = open("./test.txt",O_WRONLY | O_CREAT,0644); int fd5 = open("./test.txt",O_WRONLY | O_CREAT,0644); printf("fd1:%d,fd2:%d,fd3:%d,fd4:%d,fd5:%d\n",fd1,fd2,fd3,fd4,fd5); close(fd1);close(fd2);close(fd3);close(fd4);close(fd5); }
运行结果是从3开始的连续数字,看起来像一个数组,就是少了0、1、2
其实0、1、2在我们学习C语言时都学过,分别是 :
标准输入流 -> 键盘(stdin)
标准输出流 ->显示器(stdout)
标准错误流 ->显示器(stderr)
这三种流我们也把他们看成打开的文件,并且他们三个是默认就打开的,是执行的进程继承bash的
那么加上这三个流和上面运行结果的fd,可以看成一个数组
-
那打开多个文件,就会有多个struct file,操作系统为了方便管理,会把它们组织成数据结构:struct files_struct,
-
而这个数据结构中存在着一个数组:struct file array[ ]* ,是一个指针数组,指向的就是每个文件的struct file。
-
而在进程的PCB中,就包含了 struct files_struct的指针
所以整体应该是这样:
也就是每一个进程PCB都有一个 struct files_struct指针指向files struct,files_struct中存在着一个指针数组,当一个文件打开时就会形成一个struct file代表这个文件属性,指针数组就会存储这个结构体的地址
所以文件描述符就是,调用open打开文件时,返回的 struct_file存入数组位置的下标
fd本质是内核中,进程和文件关连的数组下标
所以close、read、write能对指定文件进行相关操作是因为传入了fd,通过下标号找到文件信息
一切皆文件
上面说了0、1、2分别代表输入流、输出流、错误流存在数组中,也就是说明它们也是文件,而他们分别对应的是键、显示器、显示器,
-
这些外设的输入输出方法由驱动完成并提供,所以他们的输入输出方法肯定是不一样的;
-
但是他们打开时也会提供一个struct file来描述这个外设的一些信息,但是他们的输入输出方法还没统一;
-
所以在每个结构体里设置两个函数指针,指向驱动的输入输出函数;
有了这个指针,那么在上层看来所有的struct file没区别,不用关心他们是什么,反正调用接口都一样
所以在上层看来,这些都是文件,也就是一切皆文件
所以在Linux中一切东西都可以通过文件的方式访问、管理。也就可以说一切皆文件。
( 包括之前说过的,一个程序的本质也是文件 = 代码+数据)
文件描述符的分配规则
既然 0、1、2也是文件描述符,那么我们把它们关闭,在打开我们自己的文件会发生什么?
请看代码:
void test4()
{
close(2);
close(0);
int fd = open("./test.txt",O_WRONLY|O_CREAT,0644);
if(fd < 0)
perror("open:");
printf("fd:%d\n",fd);
}
说明:文件描述符的分配规则是,是最小下标且空闲的位置,再返回给新fd使用
重定向
既然1代表的是标准输出流stdout,那么我们打开一个文件并关闭1会怎么样
请看代码:
void test5()
{
close(1);
int fd = open("./test.txt",O_WRONLY|O_CREAT,0644);
printf("fd:%d\n",fd);
printf("hello\n");
}
我们知道printf就是向屏幕(显示器)打印,也就是stdout,而stdout具体是一个文件指针
这个FILE本质上就是一个结构体名字typedef出来的,而stdout指向的结构体中固定有一个fd(1),这里可以验证一下
void test8()
{
printf("stdin->%d\n",stdin->_fileno);
printf("stdout->%d\n",stdout->_fileno);
printf("stderr->%d\n",stderr->_fileno);
}
_fileno就是固定记录fd的
虽然close(1)后把1给新的fd使用了,但是stdout绑定的是1,而printf是向stdout输出,stdout的文件描述固定是1,就会去数组找1这个位置的struct,所以就输出到了test.txt里
(可用fprintf验证,这里就不写了)
由此可以推出:stdout、stdin、stderr的fd一定绑定的是0、1、2,而他们相关的函数一定会去找0、1、2位置的struct file
void test6()
{
close(0);
int fd = open("./test.txt",O_RDONLY);
printf("fd:%d\n",fd);
char str[128];
while( fgets(str,127,stdin) ) //每次读128个(数组下标是0开始要-1),到0结束
{
printf("%s",str);
}
close(fd);
}
关闭stdin,打开test.txt并把文件描述符1给它,用fgets函数从stdin上读取字符,而stdin指向一个结构体,结构体内封装了fd==1,所以等于把之前写入test文件里的的数据写入到str数组里
重定向需要关闭再打开等等操作比较麻烦,还可以使用dup2这个函数来重定向
dup2重定向
用法如下:
fork后的文件描述符
我们利用fork创建了子进程后,使用进程替换不会影响这些文件打开操作,但是task_struct{mm_struc、struct files_struct* 等等}这些些结构属于的是内核,所以子进程也会有struct files_struct(例如子进程父进程都有页表一个道理),而子进程以父进程为模板初始化也就是说子进程的文件描述符对应的文件struct file是一样的。
对于指针这里只是浅拷贝,例如每个进程都打开了stdout、stdin、stderr都从bash一直往下继承的。