0%

系统级I/O

输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过 程。输人操作是从 I/O 设备复制数据到主存,而输出操作是从主存复制数据到 I/O设备。

学习 Unix I/O意义 :

了解 Unix I/O 将帮助你理解其他的系统概念。

有时你除了使用 Unix I/O 以外别无选择。

标准 I/O 库没有提供读取文件元数据的方式,例如文件大小或文件创建时间。另外,I/O 库还存在一些问题,使得用它来进行网络编程非常冒险。

Unix I/O :

所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当 作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引 出一个简单、低级的应用接口,称为 Unix I/O, 这使得所有的输人和输出都能以一种统 一且一致的方式来执行:

打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操 作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这 个描述符。

Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为 0)、标准 输出(描述符为 1)和标准错误(描述符为 2)h头文件< unistd.h> 定义了常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_ETLENO,它们可用来代替显式的描述符值。

改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操 作,显式地设置文件的当前位置为是。

读写文件。一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置是开 始,然后将k增加到k+n。给定一个大小为讲字节的文件,当时执行读操作 会触发一个称为 end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结 尾处并没有明确的 “EOF 符号”。 类似地,写操作就是从内存复制 n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响 应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池 中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们 的内存资源。

文件:

普通文件 : 文本文件和二进制文件

文本文件:

文本文件是只含有 ASCII 或 Unicode 字符的普通文件

二进制文件:

所有其他的文件

目录:

目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件

打开和关闭文件 :

1
2
int open(char *filename, int flags, mode_t mode) 
返回:若成功则为新文件描述符,若出错为一1。

open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总 是在进程中当前没有打开的最小描述符。

flags:

O_RDONLY:只读。 O_WRONLY: 只写。 O_RDWR:可读可写

O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件。

O_TRUNC:如果文件已经存在,就截断它。

O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。

mode 参数指定了新文件的访问权限位:

作为上下文的一部分,每个进程都有一个 umask, 它是通过调用 umask 函数来设置 的。当进程通过带某个 mode 参数的 open 函数调用来创建一个新文件时,文件的访问权 限位被设置为 mode & ~ umask 。

1
2
int close(int fd);
返 回:若 成 功 则 为 0, 若 出 错 则 为-1。

进程通过调用 close 函数关闭一个打开的文件。

读 和 写 文 件 :

1
2
3
4
5
ssize_t read(int fd, void *buf, size_t n);
返 回:若 成 功 则 为 读 的 字 节 数,若 EOF 则 为 0, 若 出 错 为 一1。
ssize t write(int fd, const void *buf, size_t n);
返 回:若 成 功 則 为 写 的 字 节 数,若 出 错 则 为 一1。

过调用 lseek 函数,应用程序能够显示地修改当前文件的位置

文件从当前文件位置开始只含有 20 多个字节,而我们以 50 个字节的片进行读取。这样一来,下一个 read 返回的不足 值为 20 . 此后的 read 将通过返回不足值 0 来发出 EOF 信号。

从终端读文本行。如果打开文件是与终端相关联的(如键盘和显示器),那么每个 read 函数将一次传送一个文本行,返回的不足值等于文本行的大小。

用 RIO 包健壮地读写:

像网络程序这样容易出现不足值的应用中,RIO 包提供了方便、健壮和高效的 I/O。RIO 提供了两类不同的函数:

无缓冲的输入输出函数。这些函数直接在内存和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。

带缓冲的输入函数。这些函数允许你高效地从文件中读取文本行和二进制数据,这 些文件的内容缓存在应用级缓冲区内,类似于为printf 这样的标准 I/O函数提供 的缓冲区。带缓冲的RIO输人函数是线 程安全的,它在同一个描述符上可以被交错地调用。例如,你可以从一 个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行。

RIO 的无缓冲的输入输出函数 :

1
2
3
ssize t rio_readn(int fd, void *usrbuf,size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
返回:若成功则为传送的字节数,若 EOF 则为 0(只对 rio_reacin 而言),若出错则为一1。

rio_read 函数在遇 到 EOF 时只能返回一个不足值。rio_writen 函数决不会返回不足值。

对同一个描述符, 可以任意交错地调用 rio_readn 和 rio_writen。

如果 rio_readn 和 rio_ writen 函数被一个从应用信号处理程序的返回中断,那么每个函数都会手动地重启 read 或 write。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ssize_t rio_readn(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;

while (nleft > 0){
if ((nread = read(fd, bufp, nleft))< 0){
if (errno == EINTR)/* Interrupted by sig handler return */
nread = 0; /* and call read()again */
else
return -1; / errno set by readO */
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return(n - nleft); /* Return >= 0 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize t rio_writen(int fd, void *usrbuf, size_t n)
{
size t nleft = n;
ssize t nwritten;
char *bufp = usrbuf;
while(nleft > 0){
if ((nwritten = write(fd, bufp, nleft))<= 0){
if (errno == ENTRY)/* Interrupted by sig handler return */
nwritten = 0 /* and call write()again */
else
return -1;/* errno set by write()*/
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}

RIO 的带缓冲的输入函数 :

是调用一个包装函数(rio_readlineb), 它从一个内部读缓冲区复制一个 文本行,当缓冲区变空时,会自动地调用 read 重新填满缓冲区。对于既包含文本行也包含二 进制数据的文件,我们也提供了一个 rio_readn 带缓冲 区的版本,叫做 rio_readnb,它从和 rio_readlineb —样的读缓冲区中传送原始字节。

1
2
3
4
5
6
void rio_readinitb(rio_t *rp, int fd);
返 回: 无。
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n):
返 回:若 成 功 则 为 读 的 字 节 数,若 EOF 则 为 0, 若 出 错 则 为 一1。

每打开一个描述符,都会调用一次 rio_readinitb 函数。它将描述符 fd 和地址 rp 处的一个类型为 rio_t 的读缓冲区联系起来。

对同一描述符,对 rio_readlineb 和 rio_readnb 的调用可以任意交叉进行。

对这些带缓冲的函数的 调用却不应和无缓冲的 rio_readn 函数交叉使用。

1
2
3
4
5
6
7
8
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio cnt = 0;
rp->rio—bufptr = rp->rio_buf;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ssize_t rio_readlineb(rio_t *rp,void *usrbuf, size t maxlen)
{
int n, rc;
char c, *bufp = usrbuf;
for (n = 1; n < maxlen; n++){
if ((rc = rio_read(rp, &c, 1))== 1){
*bufp++ = c;
if (c == '\n'){
n++;
break;
}
}else if (rc == 0){
if (n == 1)
return 0; /* EOF, no data read */
else
break; /* EOF, some data was read */
}else
return -1; /* Error
}
*bufp = o;
return n-1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ssize t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;
while (nleft > 0){
if ((nread = rio_read(rp, bufp, nleft))< 0)
return -1; /* errno set by read()*/
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* Return >= 0 */
}

读 取 文 件 元 数 据 :

应用程序能够通过调用 stat 和 fstat 函数,检索到关于文件的信息(有时也称为文 件的元数据 )

1
2
3
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
返回:若成功则为 0,若出错则为 _1。

讨论 Web 服务器时,会需要 stat 数据结构中的 st_mode 和 st_size 成员

st_mode 成员则编码了文件访问许可位 , Linux 在 sys/stat.h 中定义了宏谓词来确定 st_mode 成员 的文件类型:

S_ISREG(m)。这是一个普通文件吗?

S_ISDIR(m)0 这是一个目录文件吗?

S _ISSOCK(m)。这是一个网络套接字吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "csapp.h"
int main (int axgc, char **argv)
{
struct stat stat;
char *type, *readok;
Stat(argv[l], festat);
if (S ISREG(stat.st_mode)) /* Determine file type */
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR))/* Check read access */
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s\nM, type, readok);
exit(0);
}

读 取 目 录 内 容 :

1
2
3
DIR *opendir(const char *name);
返 回:若 成 功 ,则 为 处 理 的 指 针;若 出 错,则 为 NULL。

函数 opendir 以路径名为参数,返回指向目录流的指针。流是对 条目有序列表的抽象,在这里是指目录项的列表。

1
2
3
struct dirent *readdir(DIR *dirp);
返 回: 若 成 功 ,则 为 指 向 下 一 个 目 录 项 的 指 针;若 没 有 更 多 的 目 录 项 或 出 错,则 为 NULL。

每次对 readdir 的调用返回的都是指向流 dirp 中下一个目录项的指针,或者,如果 没有更多目录项则返回 NULL。

1
2
3
int closedir(DIR *dirp);
返回:成功为 0; 错误为一 1。

函数 closedir 关闭流并释放其所有的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "csapp.h"
int main(int argc, char **argv)
DIR *streamp;
struct dirent *dep;
streamp = Opendir(argv[1])
errno = 0;
while ((dep = readdir(streamp)) != NULL){
printf("Found file: %s\n", dep->d name)
}
if (errno != 0)
unix_error("readdir error");
Closedir(streamp)
exit(0);
}

共享文件 :

内核用三个相关的数据结构来表示打开的文件:

描述符表。每个进程都有它独立的描述符表,它的表项是由进程 打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。

文件表。打开文件的集合是由一张文件表来表示的,所有的进程共享这 张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、 引用计数 (即当前指向该表项的描述符表项数),以及一个指向 v-node 表中 对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不 会删除这个文件表表项,直到它的引用计数为零。

v - node 表。同文件表一样,所有的进程共享这张 v - node 表。每个表 项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size 成员。

I/O 重定向 :

1
2
int dup2(int oldfd, int newfd);
返 回:若 成 功 则 为 非 负 的 描 述 符,若 出 错 则 为 一1。

dup2 函数复制描述符表表项 oldfd 到描述符表表项 newfd,覆盖描述符表表项 new - fd以前的内容。如果 newfd 已经打开了,dup2会在复制 oldfd 之前关闭 newfd。

标准 I/O :

C 语言定义了一组高级输人输出函数,称为标准 I/O 库,为程序员提供了 Unix I/O 的较高级别的替代。这个库(libc)提供了打开和关闭文件的函数(fopen 和 fclose)、读 和写字节的函数(fread 和 fwrite)、读和写字符串的函数(fgets 和 fputs),以及复杂 的格式化的 I/O 函数(scanf 和 printf)。

标准 I/O 库将一个打开的文件模型化为一个流。对于程序员而言,一个流就是一个指 向 FILE 类型的结构的指针。每个 ANSI C 程序开始时都有三个打开的流 stdin、stdout 和 stderr,分别对应于标准输人、标准输出和标准错误:

1
2
3
extern FILE *stdin; /* Standard input(descriptor 0) /
extern FILE *stdout; /* Standard output(descriptor 1)*/
extern FILE *stderr; /* Standard error(descriptor 2)*/

类型为 FILE 的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和 RIO 读缓冲 区的一样:就是使开销较高的 Linux I/O 系统调用的数量尽可能得小。例如,假设我们有 一个程序,它反复调用标准 I/O 的 getc 函数,每次调用返回文件的下一个字符。当第一 次调用 getc 时,库通过调用一次 read 函数来填充流缓冲区,然后将缓冲区中的第一个 字节返回给应用程序。只要缓冲区中还有未读的字节,接下来对 getc 的调用就能直接从 流缓冲区得到服务。

我 该 使 用 哪 些 I/O 函 数 :

只要有可能就使用标准 I/O。 (stat 在标准 I/O 库中没有与它对应的函 数 )

不要使用 scanf 或 rio_readlineb 来读二进制文件。 二进制文件可能散布着很多 Oxa 字节,而这些字节又与终止文本行无关。

对网络套接字的 I/O 使用 RIO 函数。

标准 I/O 流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输人和 输出。然而,对流的限制和对套接字的限制,有时候会互相冲突,而又极少有文档描述这 些现象:

限制一 :跟在输出函数之后的输入函数。如果中间没有插人对 fflush、fseek、 fsetpos 或者 rewind 的调用,一个输人函数不能跟随在一个输出函数之后。 fflush 函数清空与流相关的缓冲区。后三个函数使用 Unix I/O lseek 函数来重置 当前的文件位置。

限制二:跟在输入函数之后的输出函数。如果中间没有插人对 fseek、 fsetpos 或 者 rewind 的调用,一个输出函数不能跟随在一个输人函数之后,除非该输入函数 遇到了一个文件结束。

这些限制给网络应用带来了一个问题,因为对套接字使用 lseek 函数是非法的。对流 I/O 的第一个限制能够通过采用在每个输人操作前刷新缓冲区这样的规则来满足。然而, 要满足第二个限制的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来 读,一个用来写 :

1
2
3
FILE *fpin, *fpout;
fpin = fdopen(sockfd, "r");
fpout = fdopen(sockfd, "w"):

这种方法也有问题,因为它要求应用程序在两个流上都要调用 fclose,这样才 能释放与每个流相关联的内存资源,避免内存泄漏:

1
2
fclose(fpin);
fclose(fpout)

这些操作中的每一个都试图关闭同一个底层的套接字描述符,所以第二个 close 操作 就会失败。

建议你在网络套接字上不要使用标准 I/O 函数来进行输人和输出,而要使 用健壮的 RIO 函数。如果你需要格式化的输出,使用 sprintf 函数在内存中格式化一个 字符串,然后用 rio_writen 把它发送到套接口。如果你需要格式化输人,使用 rio_ readlineb 来读一个完整的文本行,然后用 sscanf 从文本行提取不同的字段。