主要参考CSAPP
简介
所有语言运行时系统都提供执行IO较高级别工具。例如,ANSI C提供了标准IO库,包含像printf和scanf这样执行带缓冲区的IO函数。C++重载了 >> 和 << 提供类似功能。在Linux系统中,使用是通过内核提供的系统级Unix IO函数来实现这些较高级别的IO函数。
Unix IO
一个Linux文件就是一个m个字节的序列,所有的IO设备都没模型话为文件,而所有的输入和输出都被当作相应文件的读写和执行。通过这种方式,所有的输入和输出都能以一种统一且致的方式来执行。
- 打开文件:一个应用程序通过要求内核打开相应文件,来宣告它想要访问一个IO设备。内核返回一个非负整数,叫做描述符,它在后续对此文件的所有操作中标示这个文件。内核记录有关这个打开文件所有信息。应用程序只需要记住这个描述符
- Linux shell创建的每个进程开始时都有三个打开文件:标准输入(描述符为0),标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO, STDOUT_FILENO 和 STDERROR_FILENO。他可以用来代替显示的描述符。
- 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k。初始为0.这个文件位置是从文件开头其实的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置k。
- 读写文件:一个读操作就是从文件复制 n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k + n。给定一个大小为m字节的文件,当k >= m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的”EOF符号”
- 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的资源。
打开和关闭文件
进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:
#incldue <sys/types.h>
#incldue <sys/stat.h>
#incldue <fcntl.h>
int open(char *filename, int flags, mode_t mode);
复制代码
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:
- O_RDONLY: 只读
- O_WRONLY: 只写
- O_RDWR: 可读可写
例如,下面的代码说明如何以读的方式打开一个已存在的文件
fd = Open("foo.txt", O_RDONLY, 0);
复制代码
flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:
- O_CREAT: 如果文件不存在,就创建一个截断的文件。
- O_TRUNC: 如果文件已经存在,就截断它
- O_APPEND: 每次写操作之前,设置文件位置到文件的结尾处。
例如,下面代码说明是如何打开一个已存在的文件,后面添加一些数据:
fd = open("foo.txt", O_WRONLY | O_APPEND, 0);
复制代码
mode参数指定了新文件的访问权限位。
作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open参数调用来创建一个新文件,文件的访问权限位被设置为mode & ~ umask。例如,假设我们给定下面的mode 和 umask的值:
#define DEF_MODE S_IRSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
复制代码
接下来,下面的代码片段创建一个新文件,文件的拥有者有读写权限,而所有其他的用户都有读权限:
umask(DEF_UMASK);
fd = Open("foo.txt", O_CREAT|O_TEUNC|O_WEONLY, DEF_MODE);
复制代码
最后,进程通过调用close函数关闭一个文件
#include <unistd.h>
int close(int fd);
复制代码
如下:
int fd1, fd2;
fd1 = Open("foo.txt", O_RDONLY, 0);
Close(fd1);
fd2 = Open("foo.txt", O_RDONLY, 0);
printf("fd2 = %d \n", fd2);
exit(0);
复制代码
读和写文件
应用程序是通过分别调用read和write函数来执行输入和输出的。
#include <unistd.h>
// 若成功则为读的字节数,若EOF则为0,若出错为-1
ssize_t read(int fd, void *buf, size_t n);
// 若成功则为写的字节数,若出错则为-1
ssize_t write(int fd, const void *buf, size_t n);
复制代码
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
例如,一个程序用read和write调用一次一个字节从标准输入复制到标准输出
char c;
while (Read(STDIN_FILENO, &c, 1) != 0) {
Write(STDOUT_FILENO, &c, 1);
}
exit(0);
复制代码
这时候就可以在输入栏里面输入,确认回车后控制台就会输出所输入的。
hello world unix io
hello world unix io
复制代码
ssize_t和size_t有什么区别
read函数有一个size_t的输入参数和一个ssize_t的返回值。那么这两种方式有什么区别呢?在x86-64系统中,size_t被定义为unsigned long,而ssize_t被定义为long。因为read会出错,会返回-1所有要用long类型来表示
在某些情况下,read和write传送的字节比应用程序要求的要少。这些不足值不表示有错误。出现这样的情况原因有:
- 读时遇到EOF。当我们的缓冲区较大而文件字节数较少时,会有这样的问题
- 从终端读文本行。如果打开文件是和终端关联的,那么read将一次传送一个文本行,返回的不足值等于文本行大小
- 读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延时会引起read和write返回不足值。对Linux管道调用read和write时,也可能出现不足值。
实际上,除了EOF,当你读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。然后,如果你想创建健壮的诸如Web服务器这样的网络应用,就必须通过反复调用read和write处理不足值,直到所需要的字节都传送完毕。