输入/输出(I/O)是在内存和外设之间复制数据的过程。输入操作是从外设复制数据到内存,输出操作是从内存复制数据到外设。
所有语言提供的I/O库,如C语言的printf、scanf,C++重载的>>、<<,都被称为标准I/O接口,它们都是在操作系统提供的系统级I/O接口的基础上实现的。
Linux系统秉持着"一切皆文件"的原则,将所有的I/O设备,如磁盘、键盘、网络等,都模型化为统一的"文件"。
其最大的好处在于:允许用户使用同一套系统I/O接口对这些设备进行输入/输出操作。
int open(const char *pathname, int flags, .../*mode_t mode*/)
pathname:目标文件的绝对路径或相对当前工作目录的路径flags:打开方式,包括:O_RDONLY:只读
O_WRONLY:只写
O_RDWR :可读可写
O_APPEND:追加,必须和O_WRONLY搭配使用,即O_APPEND | O_WRONLY
O_CREAT:如果目标文件不存在,则以pathname为路径创建该文件
注:flags可以是几个标志按位或,比如O_CREATE | O_WRONLY表示写文件,如果该文件不存在,则先创建。
mode:可变参数列表中的参数,如果需要创建文件,则需要传该参数(8进制)作为新文件的默认权限,最终权限由“默认权限 & ~umask”决定,其中umask为文件权限掩码ssize_t read(int fd, void *buf, size_t count)
fd:目标文件描述符buf:输出型参数,用作存储缓冲区count:最多读取的字节数ssize_t类型的整数(本质为long类型)。如果成功,则返回本次实际读取到的字节数,0表示遇到EOF,失败则返回-1ssize_t write(int fd, const void *buf, size_t count)
fd:目标文件描述符buf:输入型参数,写入内容的存储缓冲区 count:要写入的字节数,一般为sizeof(buf)ssize_t类型的整数(本质为long类型),如果成功,则返回本次实际写入的字节数,失败则返回-1int close(int fd)
将文件描述符为fd的文件关闭,成功则返回0,失败则返回-1。
注:所谓关闭,就是将当前占用fd的文件相关结构体释放,之后该fd还可被分配给新打开的文件。
文件描述符,简称fd,本质是一个非负整数,用户可以使用它来进行对应文件的I/O操作。
Linux内核为了记录每个进程打开的哪些文件,在进程控制块task_struct中保存了一个维护已打开文件的结构体files_struct,该结构体内部维护了一个文件记录表**fd_array**用来保存文件的具体信息。而所谓的fd,本质上就是这个文件记录表的数组下标,内核通过fd就可以索引到具体的文件。


fd_array数组中存储了file结构体,该结构体包含了各种文件相关信息。其中,f_op是一个file_operations结构体,该结构体维护了操作该文件的各种函数指针,如read、write等。
由此可以看出:
Linux下将各种I/O设备都模型化为文件系统的一个结构体,且根据不同类型的文件,操作系统会为它注册不同的操作方法。
Linux下每个进程都会默认打开三个“文件”:标准输入、标准输出和标准错误,它们的文件描述符分别为0、1、2。
之后再打开的文件,它的文件描述符是当前未被占用的最小的fd。
这三个默认文件在C语言中分别对应FILE*类型的文件指针:stdin、stdout、stderr。

C语言的FILE结构体原型是_IO_FILE:

其中的_fileno保存文件描述符,其它的各类char*类型指针对应C语言维护的用户级缓冲区等信息。
用户级缓冲区由高级语言提供,是进程运行时用户空间的一段虚拟内存,如C语言的用户级缓冲区由FILE结构体维护。当用户向“文件”中读写时,优先使用这部分缓冲区的内容。
内核级缓冲区是由操作系统在内核空间为进程打开的文件所维护的,每个文件都拥有自己的缓冲区。
为了方便理解,以读写为例:
1、当用户使用标准I/O接口进行“读”操作时,首先操作系统会查看当前的用户缓冲区是否有可读数据,如果没有,则查看内核缓冲区是否有数据,如果还没有,则先将数据从外部I/O设备拷贝至内核缓冲区,再将内核缓冲区的数据拷贝至用户缓冲区;

2、当用户使用标准I/O接口进行“写”操作时,首先将数据拷贝至语言层提供的用户缓冲区。当满足语言规定的刷新策略时(比如行缓冲、全缓冲),操作系统会将用户缓冲区的数据拷贝至内核缓冲区,之后再根据内核提供的刷新策略(比如积累到一定量)将数据写入磁盘、网卡等外部I/O设备。

当读写的数据填满缓冲区时才进行对应的I/O操作,典型代表是对磁盘文件的读写。
当输入和输出遇到换行符时才进行I/O操作,典型代表是键盘、显示器。
不设立缓冲区,典型代表是stderr,方便错误信息快速地显示出来。
进程结束后自动刷新、强制刷新(fflush()接口).
printf("printf()\n");
fputs("fputs()\n", stdout);
char* buffer = "write()\n";
write(1, buffer, strlen(buffer));
fork();
当直接在命令行运行该程序时,输出结果为:

当把输出结果重定向至文件中时,文件内容为:

可以看到,文件中标准I/O接口printf和fputs分别输出了两次,而系统调用接口write仅输出一次,原因在于:
进程会创建父进程的虚拟地址空间副本,那么用户空间的用户缓冲区也理所应当地被子进程拿到。由于写时拷贝的存在,父子进程的用户缓冲区内容相同但相互独立,因此会被刷新两次;
write属于系统调用接口,而系统调用接口会直接将数据拷贝至内核缓冲区,不存在用户级缓冲区;
使用./test命令时,printf和fputs会向显示器文件写入数据。而显示器文件的刷新策略是行缓冲,因此在遇到’\n’时,用户缓冲区的数据被立即刷新至内核缓冲区,并由内核决策何时将其写入显示器文件。
使用重定向>时,printf和fputs会向普通文件中写入数据。而普通文件的刷新策略是全缓冲,只有在用户缓冲区满时或进程结束才会将数据刷新至内核缓冲区,并由内核决策何时将将其写入普通文件。
对于内核缓冲区:
对于用户缓冲区:
向内核缓冲区读写数据的标准I/O接口本质上使用了read()、write()这些系统调用接口。每当调用系统接口时,CPU都要从用户态切换至内核态,而这种切换会有时间消耗。当I/O较为频繁时,CPU状态切换的消耗就会相应叠加。因而设立用户缓冲区,减少因为系统调用而进行状态切换的次数。
为了方便用户将命令的输出结果保存到文件而不是直接在显示器上打印,bash提供了两个重定向操作符
>和>>;
命令 > 文件名 将对应文件内容清空,然后将命令的运行结果写入命令 >> 文件名在对应文件已有内容后追加int dup2(int oldfd, int newfd) // 失败返回-1
该函数本质上修改了当前进程控制块task_struct的文件记录表fd_array:先将原先newfd对应的文件结构体被删除,再将fd_array[oldfd]复制到fd_array[newfd],使得oldfd和newfd对应同一个文件
注:一个文件可以有不止一个fd与之对应。
每个文件描述符都对应一个文件的描述信息用于操作文件。而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件
进程创建伊始会打开三个默认文件,其中fd=1的文件是显示器的标准输出文件。
当调用dup2(fd, 1)时,标准输出文件的1号文件描述符对应的文件结构体被描述符为fd的新文件结构体覆盖,而printf这些I/O接口默认会将结果输出到1号文件,因此程序的运行结果就被写入到fd对应的文件了。
int fd = open("f.txt", O_CREAT | O_WRONLY, 0644);
dup2(fd, 1);
printf("dup2()\n");
或者根据dup2()的原理,将代码改写成:
close(1);
// 注:将1号文件close后,新打开文件的fd自然就是1,那么printf就会将结果写入该文件了。
int fd = open("f.txt", O_CREAT | O_WRONLY, 0644);
printf( "dup2()\n");
可以看到,原本应当输出到控制台的信息被重定向到了文件中。
