Linux_系统I/O

C文件I/O中,用到过fopen,fclose,fread,fwrite等函数,这些函数都是C标准库的函数,称之为库函数,这都是已经封装好的。而open,close,read,write,lseek等都属于系统提供的接口,称之为系统调用接口。所以库函数的底层实现,其实还是系统调用,不过库函数开发人员更好使用。接下来就来谈谈系统中文件处理。

1.文件描述符fd

文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2.

0,1,2对应的物理设备一般是:键盘,显示器,显示器所以标准输入其实还可以采用直接使用文件描述符的方式


所以,现在知道,文件描述符其实就是从0开始的整数,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上文件描述符就是该数组的下表。只要拿着文件描述符,就可以找到相对应的文件。

2.文件描述符的分配原则

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(){
    int fd = open("myfile",O_RDONLY);
    if(fd < 0){
        perror("fd error\n");
        return 1;
    }
    printf("fd:%d\n",fd);

    close(fd);
    return 0;
}

输出结果是3.关闭fd:0或者fd:2,发现结果是0或者2,由此我们可以知道,文件描述符的分配规则,在file_struct数组中,找到当前没有被使用的最小的一个小标,做为新打开的文件的文件描述符。

3.重定向

我们关闭了0,2,那么关闭1呢,我们都知道1是stdout,标准输出,一般对应着我们的屏幕,接下来关闭1试试:

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(){
    close(1);
    int fd = open("myfile",O_WRONLY|O_CREAT,0644);
    if(fd < 0){
        perror("fd error\n");
        return 1;
    }
    printf("fd:%d\n",fd);
    fflush(stdout);

    close(fd);
    return 0;
}

结果我们发现,本该输入到屏幕上的内容,输出到了myfile里。而且fd=1,那么我们把这种现象叫输出重定向,常见的输出重定向有>,>>,<等。



C语言中,printf是C标准库中的I/O函数,一般都是写到了stdout中,但是之前我们说了,库函数还是封装了系统调用,所以stdout底层访问文件的时候,找的还是fd:1,但我们关掉了fd:1,打开了一个新文件,那么fd:1就变成了我们打开的文件,可是printf不知道此时显示器的地址已经是myfile的地址了。因此输出的任何消息都会往myfile中写入,进而完成了输出重定向。

4.FILE

因为I/O相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。

所以C库中的FILE结构体内部,必定封装了fd。我们可以研究一下

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(){
    const char* msg0 = "hello printf\n";
    const char* msg1 = "hello fwrite\n";
    const char* msg2 = "hello write\n";

    printf("%s",msg0);
    fwrite(msg1,strlen(msg1),1,stdout);
    write(1,msg2,strlen(msg2));

    fork();
    return 0;
}

输出结果:

[syf@dreame bit]$ ./a.out
hello printf
hello fwrite
hello write

然而我们使用输出重定向:

[syf@dreame bit]$ ./a.out > file
[syf@dreame bit]$ cat file
hello write
hello printf
hello fwrite
hello printf
hello fwrite

结果竟然发现printf和fwrite都输出了两次,而write只输出了一次。其实原因就是因为write是系统调用。而另外两个都是库函数,总结原因如下:

 
 

(1)一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

(2)printf fwrite库函数会自带缓冲区,当重定向到普通文件是,数据的缓冲方式由行缓冲变成了全缓冲

(3)我们放在缓冲区的数据,不会立即被刷新,甚至fork之后

(4)进程退出之后,统一刷新,写入文件,但是fork时,父子进程数据会发生写时拷贝,所以父进程准备刷新的时候,子进程也就有了同样的一份数据,随机产生两份数据

(5)write没有变化,可见系统调用没有所谓的缓冲区

那么综上所述,printf fwrite库函数都会自带缓冲区,而write系统调用没有带缓冲区。另外,我们所提到的缓冲区,其实都属于用户级缓冲区。然而操作系统为了提升整机性能,也给内核提供相应内核级缓冲区,不过我们现在不用了解。而且只有库函数调用产生缓冲区问题,系统调用是不会有的,那么其实这个缓冲区就是有C标准库提供的。

猜你喜欢

转载自blog.csdn.net/qq_40425540/article/details/80027033