系统级I/O - CS:APP 第十章

系统级I/O - CS:APP 第十章

Linux 所有的IO设备都被模型化为文件,所有的输入输出都被抽象成文件的读写。这种将设备映射成文件的方式,允许内核提供一些低级的函数接口还读写,被称为Unix IO。

在Linux中,较高级的IO函数(C标准库IO)是由内核提供的系统级Unix IO来实现的。

文件描述符

  • 定义:一个应用程序要通过内核打开文件,内核返回一个非负小整数,叫做文件描述符,应用程序要操作文件,只需要知道文件描述符即可。

  • Linux Shell 创建的进程会打开三个文件:

    0:标准输入;也可以使用定义在unistd.h 中的STDIN_FILENO 来显示表述

    1:标准输出;STDOUT_FILENO

    2:标准错误;STDERR_FILENO

  • EOF:如果一个字节数为m的文件,一个读操作,读到k字节处。如果k >= m,则触发一个End of file条件,应用程序可以检测这个条件,但文件末尾并不是真的有EOF符号。

文件类型

  • 普通文件 regular file : 包含任意数据,系统并不会区分二进制文件或者文本文件,那时应用程序需要区分的。
  • 目录 directory :是一个包含一组连接的文件,每个链接都映射到一个文件。有两个特殊的链接,每个目录文件一定会有,他们是... 分别代表自己和上层目录。
  • 套接字 socket :用来和其他进程进行跨网络通信的文件。

系统级IO函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// 打开和关闭文件
int open(char * filename, int flags, mode_t mode);
/*
flags: 文件的打开方式
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 可读可写

O_CREAT 如果文件不存在,创建
O_TRUNC 如果文件已存在,截断它(即覆盖原文件)
O_APPEND 添加到文件末尾
不同的flag可以使用管道符| 连接使用

mode:
一般为0
*/
int close(int fd);

// 读写
ssize_t read(int fd, void *buf, size_t n);
// 返回值:若成功返回-1,EOF返回0,出错返回-1
ssize_t write(int fd, const void *buf, size_t n);
// 返回值:若成功返回写的字节数,出错返回-1

ssize_t :signed size type

read 函数会返回不足值(short count) ,即返回的数目并不是要求的size_t n

可能会返回不足值得情况:

  • 读时遇到EOF。假设我们准备读一个文件,该文件从当前文件位置开始只含有20多个字节,而我们以50个字节的组块(chunk)进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回O发出EOF信号。
  • 从终端读文本行。如果打开文件是与终端相关联的(例如,键盘和显示器),那么每个rad函数将一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和写网络套接字(socket)。如果打开的文件对应于网络套接字(12.3.3节),那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。对Unix管道(pipe)调用read和wte,也有可能出现不足值,这种进程间通信机制不在我们讨论的范围之内。

RIO包函数

RIO(Robust I/O):用于网络编程中,会自动处理不足值的情况。

总览:

1
2
3
4
5
6
7
8
// 不带缓冲区的版本
ssize_t rio_readn(int fd, void *usrbuf, size_t n); // 代替read
ssize_t rio_writen(int fd, void *usrbuf, size_t n); // 代替write

// 带缓冲区的版本
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); // 带缓冲区的rio_readn

带缓冲区和不带缓冲区的优劣:

带缓冲区:由于每次调用read都要进入内核模式进行系统调用,会比较浪费时间,因此带缓冲区可以在缓冲区为空的时候填满缓冲区,等下次调用的时候直接在缓冲区中取出数据,这样会节省减少不必要的系统调用。

不带缓冲区:比较快速,方便在网络上进行数据传输(臆断)

不带缓冲区的函数

rio_readn()

用来代替read 的RIO函数,无缓冲区,这个函数只有在遇到EOF时,才会返回不足值,其他情况下,绝不会返回不足值。

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_readn(int fd, void *usrbuf, size_t n)
{
size_t nleft = n; // 整个程序中,还没有被读取的字节
ssize_t nread; // 单次调用read 函数所读取的字节
char *bufp = usrbuf;

// nleft != 0
while (nleft > 0) {
if ((nread = read(fd, bufp, nleft)) < 0) { // read 函数发生错误
if (errno == EINTR) // interrupted by a sig handler return
nread = 0; // call read again
else
return -1; // unknown error
}
else if (nread == 0)
break; // EOF
nleft -= nread;
usrbuf += nread;
}

return n - nleft; // 这个程序读取的所有字节
}

rio_writen()

用来代替write 的函数,无缓冲区,绝对不可能返回不足值

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; // 单词write函数写入的字节
char *bufp = usrbuf; // 下一个将被写入的位置

while (nleft > 0) {
if ((nwritten = write(fd, bufp, nleft)) <= 0) {
if (errno == EINTR) // 被一个信号处理程序返回打断
nwritten = 0;
else
return -1; // 未知错误
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}

带缓冲区的函数

缓冲区的代码

1
2
3
4
5
6
7
8
#define RIO_BUFSIZE 8192  // 缓冲区的大小
typedef struct
{
int rio_fd; // 与缓冲区关联的文件描述符
int rio_cnt; // 缓冲区中的未读字节数
char *rio_bufptr; // 下一个未读字节地址
char rio_buf[RIO_BUFSIZE]; // 缓冲区
}rio_t;

初始化缓冲区

1
void rio_

rio_read()

带缓冲区的rio_read() 遇到错误返回-1,遇到EOF返回0,否则返回成功读取的字节数

该函数会返回不足值,除了上述几种不足值,当缓冲区的字节数小于要求的字节数时,也会返回不足值,我们其他的带缓冲区的rio函数都是基于这个函数。

该函数会首先调用read填满缓冲区,如果缓冲区内还有字节则会直接读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt; // 本次调用函数读取的字节数

while (rp->rio_cnt <= 0) { // 如果缓冲区为空,重新填满缓冲区
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (errno != EINTR) // 被信号打断,自动重启
return -1; // 未知错误
}
else if (rp->rio_cnt == 0)
return 0; // EOF
else
rp->rio_bufptr = rp->rio_buf; // 重置bufptr
}

cnt = n;
if (rp->rio_cnt < n) // 缓冲区字节数不够
cnt = rp->rio_cnt; // 返回不足值
memcpy(usrbuf, rp->rio_bufptr, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}

rio_readlineb()

读取一行,最多读取maxlen-1个字节,最后一个字节要填充'\0'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
int n, rc; // n 程序读取字节数, rc 单次rio_read 的返回值
char c, *bufp = usrbuf; // c一个临时变量,用于逐个读取,测试是否为换行

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,什么字节也没读取,直接返回0
else
break; // EOF, 但已经读取了一些字节,需要处理后返回
} else
return -1; // 未知错误
}

*bufp = 0; // 最后一位用 '\0' 填充
return n - 1;
}

rio_readnb()

rio_readn的带缓冲区版本,代码结构与rio_readn 基本相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t nleft = n; // bytes that has not been read
ssize_t nread; // read() func had read nread bytes
char *bufp = usrbuf;

// nleft != 0
while (nleft > 0) {
if ((nread = rio_read(rp, bufp, nleft)) < 0) // someting wrong
return -1; // unknown error
else if (nread == 0)
break; // EOF
nleft -= nread;
usrbuf += nread;
}

return n - nleft;
}

读取文件元数据

1
2
3
4
5
#include <unistd.h>
#include <sys/stat.h>

int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

stat() 函数以文件名作为输入,填写struct stat *buf

我们只对struct stat 的一些条目感兴趣:

  • buf->st_size 文件的大小

  • buf->st_mode 使用在sys/stat.h 的三个宏可以判断文件类型:

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

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

    S_ISSOCK(mode) 这是一个网络套接字吗?

读取目录

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);

#include <dirent.h>
struct dirent * readdir(DIR *dirp);

struct dirent {
ino_t d_ino; // inode number 文件位置
char d_name[256]; // Filename 文件名
}

IO重定位

1
2
3
4
5
6
#include <unistd.h>

int dup2(int oldfd, int newfd);
/*
复制oldfd 到newfd,如果newfd 已经打开,dup2会在复制之前关闭newfd
*/