系统级I/O
输入/输出 I/O是在主存和外部设备之间复制数据的过程。
- 输入操作是从I/O设备复制数据到主存
- 输出操作是从主存复制数据到I/O设备
文件:无格式的字节序列
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告应用程序想要访问一个I/O设备。内核返回一个当前进程中没有打开的最小描述符(非负整数),应用程序在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。
当文件偏移量 > 文件大小的时候,触发end-of-file(EOF),在文件结尾处并没有明确的“EOF符号”。(判断文件是否已经结束是通过比较当前的文件偏移量和文件长度来实现)。
关闭文件:当应用程序完成了对文件的访问之后,应用程序就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
文件
文件类型:每个Linux文件都有一个类型(7种文件类型)
- 普通文件,应用程序通常要区分文本文件和二进制文件,对内核而言两者没有区别
- 文本文件
只含ASCII或Unicode字符的普通文件。Linux文本文件包含了一个文本行序列,其中每一行都是一个以换行符结尾的字符序列。 - 二进制文件
其它文件
- 文本文件
- 目录:是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件
- 套接字
- 命名通道
- 符号链接
- 字符
- 块设备
作为其上下文的一部分,每个进程都有一个当前工作目录来确定进程在目录层次结构中的当前位置。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
成功:以NULL结尾的字符串,包含绝对路径
出错:NULL
文件操作
打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
成功:新文件描述符,总是在进程中当前没有打开的最小描述符
失败:-1
flags参数指明了进程打算如何访问这个文件:flags可以是一个或者更多位掩码的或
- O_RDONLY
- O_WRONLY
- O_RDWR
- O_CREATE:如果文件不存在,就创建它的一个截断的空文件
- O_TRUNC:如果文件已存在,就截断它
- O_APPEND:在每次写操作之前,设置文件的偏移量到文件末尾处
mode参数指定了新创建文件的访问权限位:
作为上下文的一部分,每个进程都有一个umask(可通过调用umask函数来设置)。当进程通过带某个mode参数的open函数调用来创建一个新进程时,文件的访问权限被设置为mode & ~mask
。
关闭一个打开的文件:
#include <unistd.h>
int close(int fd);
成功:0
res = 0
出错:-1
res = -1, Bad file descriptor
关闭一个已关闭的描述符会出错(返回-1,并设置errno)。
errno = 0时,strerror(errno) = “Success”
读和写文件:
#include <unistd.h>
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可以显示修改当前文件的位置
某些情况下,read和write传送的字节比应用程序要求的要少,这些不足值(返回值 < 参数n)不表示有错误,出现不足值的情况如下:
- 读时遇到EOF,所要读的参数n > 文件当前位置开始到EOF的字节数
- 从终端读文本行
- 读和写网络套接字
※RIO健壮地读写
可以通过反复调用read和write处理不足值,直到所有需要的字节都传送完毕。
Robust I/O,自动处理不足值。
RIO的无缓冲的输入输出函数
这些函数直接在内存和文件之间传送数据,没有应用级缓冲,对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
通过调用rio_readn和rio_writen函数,app可以在内存和文件之间传送数据。
/*
rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf
遇到一个EOF时只能返回一个不足值 */
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 and call read() again
nread = 0;
} else { // errno set by read()
return -1;
}
} else if (nread == 0) { // EOF
break;
}
nleft -= nread;
bufp += nread;
}
return (n - nleft); // return >= 0
}
成功:传送的字节数
EOF:0
出错:-1
/* rio_writen函数从位置usrbuf传送n个字节到描述符fd,rio_writen函数绝不会返回不足值 */
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 == EINTR) { // interrupted by sig handler return and call write() again
nwritten = 0;
} else { // errno set by write()
return -1;
}
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
成功:传送的字节数
出错:-1
上面两个函数如果被一个从应用信号处理程序的返回中断,都会手动重启。对于同一个描述符,可以任意交错地调用rio_read和rio_writen。
Q:此处的被信号处理程序的返回中断是什么情况?磁盘的IO操作会占用CPU吗?
A:
RIO的带缓冲的输入输出函数
这些函数允许高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于标准I/O提供的缓冲区。
带缓冲的RIO输入函数是线程安全的,它在同一个描述符上可以交错地使用(可以从一个描述符中读取一些文本行,然后读取一些二进制数据,接着再多读取一些文本行?如何解释)。
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; // descriptor for this internal buf
int rio_cnt; // unread bytes in internal buf
char *rio_bufptr; // next unread byte in internal buf
char rio_buf[RIO_BUFSIZE]; // internal buffer
} rio_t; // 读缓冲区
void rio_readinitb(rio_t *rp, int fd) {
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
/*
从一个内部读缓冲区复制一个文本行,当缓冲区变空时,会自动地调用read重新填满缓冲区
从文件rp读出下一个文本行(包括换行符),将它复制到内存位置usrbuf,并且用NULL(0)字符来结束这个文本行
最多读maxlen - 1个字节,余下的一个字符留给结尾的NULL字符
超过maxlen - 1字节的文本行将被截断,并用一个NULL字符结尾*/
size_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) { // EOF,no data read
return 0;
} else { // EOF,some data was read
break;
}
} else { // error
return -1;
}
}
*bufp = 0;
return n - 1;
}
/**
一个rio_readn带缓冲区的版本
*/
size_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) { // errno set by read()
return -1;
} else if (nread == 0) { // EOF
break;
}
nleft -= nread;
bufp += nread;
}
return (n - nleft); // return >= 0
}
/*是Linux read函数的带缓冲版本
如果缓冲区为空,那么会通过调用read再填满它,这个read收到一个不足值并不是错误,只不过读缓冲区是填充了一部分
一旦缓冲区非空,rio_read就从读缓冲区复制n和rp->rio_cnt中较小值个字节到用户缓冲区,并返回复制的字节数*/
ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) {
int cnt;
while (rp->rio_cnt <= 0) { // refill if buf is empty
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (error != EINTR) { // interrupted by sig handler return
return -1;
}
} else if (rp->rio_cnt == 0) { // EOF
return 0;
} else {
rp->rio_bufptr = rp->rio_buf; // reset buffer ptr
}
}
// copy min(n, rp->rio_cnt) bytes from internal buf to user buf
cnt = n;
if (rp->rio_cnt < n) {
cnt = rp->rio_cnt;
}
memcpy(usrbuf, rp->rio_buf, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}
读取文件元数据
#include <unistd.h>
#include <sys/stat.h>
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
成功:0
失败:-1
st_size 成员包含了文件的字节数大小
st_mode 成员则编码了文件访问许可位(图10-2)和文件类型,Linux在sys/stat.h中定义了宏谓词来确定st_mode 成员的文件类型。
读取目录内容
目录是一种特殊文件,其文件内容是该目录中的目录项。
目录项内容包括索引节点编号和文件名。
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
成功:指向目录流的指针。流是对条目有序列表的抽象,这里是指目录项的列表
出错:NULL
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
成功:指向流dirp中下一个目录项的指针
没有更多的目录项或出错:NULL,如果出错会设置errno,唯一能区分错误和流结束情况的方法是检查errno在调用readdir之后是否被修改过
struct dirent {
ino_t d_ino; // inode number 文件位置
char d_name[256]; // filename 文件名
};
#include <dirent.h>
int closedir(DIR *dirp); // 关闭流比释放所有资源
成功:0
错误:-1
※共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表。每个进程都有它独立的描述符表(task_struct中有ptr指向描述符表),它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项(表项:打开文件表中的一行)。
- 文件表。打开文件的集合是由一张张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括当前的文件位置、引用计数(当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。直到引用计数为0时,内核删除这个文件表表项。
- v-node表。所有进程共享v-node表,每个表项包含 stat 结构中的大多信息。
fd1和fd4通过不同的打开文件表表项来引用两个不同的文件。如下:
多个描述符也可以通过不同的文件表表项来引用同一个文件,如以同一个filename调用open函数两次。每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。如下
父子进程共享相同的打开文件表集合,因此共享相同的文件位置。在内核删除相应文件表项之前,父子进程必须都关闭了它们的描述符。如下:
I/O重定向
Linux shell 提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。>
、<
#include <unistd.h>
int dup2(int oldfd, int newfd);
成功:非负的描述符
出错:-1
dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
调用dup(4, 1)之前,fd1指向文件A。文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了。
※I/O函数选取指南
标准I/O库将一个打开的文件模型化为一个流,一个流就是一个指向FILE类型的结构的指针。类型为FILE的流是对文件描述符和流缓冲区的抽象。
流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的Linux I/O系统调用的数量尽可能得小。
指导:
- 只要可能就是用标准I/O。对磁盘和终端设备I/O来说,标准I/O函数是首选方法。
- 不要使用scanf或rio_readlineb来读二进制文件。scanf和rio_readlineb这样的函数是专门用来读取文本文件,二进制文件可能散布着很多0xa字节,而这些字节又与终止文本行无关。
- 对网络套接字的I/O使用RIO函数。
- 标准I/O流,某种意义上是全双工的(app能够在同一个流上执行输入和输出)。
Q
Q:内核打开一个文件对三个数据结构的操作?打开一个文件的详细流程?
A:
Q:进程执行write和read的时候,进程的状态是什么?
A:read和write是系统调用函数,如果调用wr,不被阻塞应该会进入内核态,运行态