用户空间I/O
Linux如何访问外设?
使用与访问文件完全一样的办法。
Linux中,将外设抽象成了一个文件
输入设备就是只读的(键盘,摄像头,传感器等)
输出设备就是可写的(显示器,打印机,广义上,就是将计算机的计算结果,影响到物理世界,都叫做输出设备)
我们将学到,Linux的三套IO函数,Unix I/O(系统级IO),RIO包,标准C库IO
Unix I/O
基于Unix的系统调用,且Linux和win都兼容
Linux文件是包含了m个字节的序列:(物理存储上是线性的)
B0,B1,…,Bk,…,Bm-1
一个有趣的事实:
Linux系统中所有I/O设备都被表示为文件:/dev/sda2 (磁盘分区),/dev/tty2 (终端)
甚至内核也被表示为文件:/boot/vmlinuz-3.13.0-55-generic(内核映像),/proc(内核数据结构)
优雅地将文件映射到设备,使得内核能够提供一个简单的接口称为Unix I/O:
打开和关闭文件:open() 和close()。
打开文件时,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。告诉系统,我操作的是哪个文件。内核维护着所有进程打开的文件,每个描述符对应一个文件
改变当前文件游标:lseek()
对于每个打开的文件,内核保持着一个文件位置k(游标),初始为 0,这个文件位置是从文件开头起始的字节偏移量。
读取和写入文件:read()和write()
文件
每个Linux文件都有一个类型(type)来表明它在系统中的角色
普通文件:包含任意数据
目录:相关文件组的索引
套接字:用于与另一台机器上的进程通信(网络)
其他文件类型
命名管道(FIFOs)(用于两个进程通信)
符号链接
字符设备和块设备
普通文件
包含任意数据
应用程序通常将其分为两类:文本文件和二进制文件
文本文件是只包含ASCII或Unicode字符的普通文件
二进制文件是其他所有类型的文件,例如,目标文件,JPEG图像
内核并不知道以上两者的区别(文件的后缀其实没啥用)
文本文件是文本行的序列,文本行是以换行符(‘\n‘)为终止标志的字符序列(换行符是0xa,与ASCII换行字符(LF)相同)
其他系统中的行结束(EOL)指示符
- Linux和Mac OS:‘\n’(0xa):换行(LF)
- Windows和Internet协议:‘\r\n’(0xd 0xa):回车(CR)后跟换行(LF)
注:linux和win的换行符不同,不要简简单单直接ctrl c v复制,不过win有自动切换功能,也就是,linux复制到win之后,自动添上回车符。
目录(相当于文件夹)
目录由一个链接数组组成,每个链接将文件名映射到一个文件
每个目录至少包含两个基本项
- . 是指向自身的链接
- .. 是指向目录层次结构中父目录的链接
用于操作目录的命令:
- mkdir:创建空目录
- ls:查看目录内容
- rmdir:删除空目录
目录层次结构
所有文件都组织成一个以根目录为锚点的层次结构,根目录名为/(斜杠)
内核为每个进程维护当前工作目录(cwd)可使用cd命令进行修改,这个一定要注意。
路径名
文件在层次结构中的位置由路径名表示
绝对路径名以’/‘开头,表示从根目录开始的路径/home/droh/hello.c
相对路径名表示从当前工作目录开始的路径../home/droh/hello.c
打开和关闭文件
下面都是一些Unix的系统调用,不是标准C的,而是操作系统的
打开
int fd; /* file descriptor */
if ((fd= open("/etc/hosts", O_RDONLY)) < 0) {
perror("open");
exit(1);
}
通过调用open函数来打开一个已存在的文件或者创建一个新文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
//返回:若成功则为新文件描述符,若出错为一1。
返回文件描述符,整数类型。又称为句柄(handler)
每次返回的都是可用的,最小的整数值作为文件描述符。
例如,已经有0,1,2这三个用了,那么下次open,返回3,再open,返回4,close 3之后,再open,返回3。
flags参数指明了进程打算如何访问这个文件:
O_RDONLY: 只读
O_WRONLY: 只写
O_RDWR: 可读可写
O_CREAT: 如果文件不存在,就创建它的一个截断的(truncated)(空)文件。
O_TRUNC: 如果文件已经存在,就截断它。
O_APPEND: 在每次写操作前,设置文件位置到文件的结尾处。
可以使用或运算|,以同时使用多个flag
mode 参数指定了新文件的访问权限位。如图:
每个进程上下文都有一个umask,它是通过调用umask函数来设置 的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为 mode & ~ umask
假设
#define DEF_UMASK S_IWGRP|S_IWOTH
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
umask(DEF_UMASK);
fd = Open("foo.txt", O_CREAT|O_TRUNC|O_WRONLY, DEF_MODE);
此代码表示:创建一个新文件,文件的拥有者有读写权限,而所有其他的用户都有读权限:
由Linux shell创建的每个进程在初始状态时与终端关联三个打开文件:
- 0:标准输入(stdin)
- 1:标准输出(stdout)
- 2:标准错误(stderr)
关闭
关闭文件通知内核你已经完成对该文件的访问
int fd; /* file descriptor */
int retval; /* return value */
if ((retval = close(fd)) < 0) {
perror("close");
exit(1);
}
返回:若成功则为0,若出错则为-1。
在多线程程序中关闭已关闭的文件是灾难的开始
规范:即使是看似无害的函数,如close(),也要始终检查返回码
读和写文件
读文件
读取文件将字节从当前文件位置复制到内存,然后更新文件位置
char buf[512];
int fd; /* file descriptor */
int nbytes; /* number of bytes read */
/* Open file fd ... */
/* Then read up to 512 bytes from file fd */
if ((nbytes = read(fd, buf, sizeof(buf))) < 0) {
perror("read");
exit(1);
}
调用read和write函数实现读写
ssize_t read(int fd, void *buf, size_t n);(返回:若成功则为读的字节数,若EOF则为0, 若出错为-1)
返回从文件fd读取到buf中的字节数。
返回类型ssize_t是有符号整数
nbytes< 0 表示发生了错误
短计数(nbytes< sizeof(buf) )是可能的,且不是错误!(后面一会说)
ssize_t和size_t 有些什么区别?
read函数有一个 size_t的输入参数和一个 ssize_t的返 回值。那么这两种类型之间有什么区别呢?
在X86-64 系统中,size_t被定义为 unsigned long,而 ssize_t(有符号的大小)被定义为 long,read 函数返回一个有符号的大小,而不是一个无符号大小,这是因为出错时它必须返回-1。因此,read的最大值减小了一半
写文件
写入文件将字节从内存复制到当前文件位置,然后更新当前文件位置
char buf[512];
int fd; /* file descriptor */
int nbytes; /* number of bytes read */
/* Open the file fd ... */
/* Then write up to 512 bytes from buf to file fd */
if ((nbytes = write(fd, buf, sizeof(buf)) < 0) {
perror("write");
exit(1);
}
ssize_t write(int fd, const void *buf, size_t n);返回:若成功则为写的字节数,若出错则为-1。
返回从buf写入到文件fd中的字节数
nbytes< 0 表示发生了错误
与读取一样,短计数是可能的,且不是错误!
关于短计数
在以下情况下可能出现短计数:
- 在读取时遇到(文件结束符)EOF
- 从终端读取文本行
- 读取和写入网络套接字
在以下情况下永远不会出现短计数:
- 从磁盘文件读取(除了EOF)
- 向磁盘文件写入
在实践中最好的方式是,在程序中始终容忍短计数出现的可能
RIO包
RIO(Robust I/O,健壮的I/O)是一组包装器,提供了高效和健壮的I/O,适用于需要处理短计数的应用程序,比如网络程序
RIO提供了两种不同类型的函数:
无缓冲的二进制数据输入和输出:rio_readn和rio_writen
有缓冲的文本行和二进制数据输入:rio_readlineb和rio_readnb
缓冲的RIO例程是线程安全的,并且可以任意交错地在相同的描述符上使用
无缓冲的输入和输出
与Unix的read和write具有相同的接口
特别适用于在网络套接字上传输数据
#include "csapp.h"
ssize_t rio_readn(intfd, void *usrbuf, size_t n);
ssize_t rio_writen(intfd, void *usrbuf, size_t n);
//Return: 若成功则为传送的字节数, 0 on EOF (rio_readnonly), -1 on error
rio_readn仅在遇到EOF时返回短计数
仅在知道要读取多少字节时才使用它
rio_writen永远不会返回短计数
可以任意交错地在相同的描述符上调用rio_readn和rio_writen
有缓冲的输入
从部分缓存在内部内存缓冲区中的文件中高效读取文本行和二进制数据
先读取一堆,到缓冲区,用的时候从缓冲区里面拿。将多次系统调用减少为1次
#include "csapp.h"
void rio_readinitb(rio_t*rp, intfd);
ssize_t rio_readlineb(rio_t*rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t*rp, void *usrbuf, size_t n);
//Return: num. bytes read if OK, 0 on EOF, -1 on error
rio_readlineb从文件fd中读取最多maxlen字节的文本行,并将该行存储在usrbuf中(特别适用于从网络套接字读取文本行)
停止条件:
- 读取了maxlen字节
- 遇到EOF
- 遇到换行符(’\n’)
有缓冲的输入(2)
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_readnb从文件fd中读取最多n字节的数据
停止条件:
读取了maxlen字节
遇到EOF
可以任意交错地在相同的描述符上调用rio_readlineb和rio_readnb
注意:不要与对rio_readn的调用交错因为先读一整个缓冲区的,然后再读没有缓冲区的,肯定就读错了
对于从文件读取的情况:
文件有关联的缓冲区,用于保存已从文件中读取但尚未被用户代码读取的字节

该缓冲区构建在Unix文件之上:

所有关于缓冲区的信息都包含在结构体rio_t中
typedef struct{
int rio_fd; /* 文件描述符*/
int rio_cnt; /* 缓冲区中未读数据的大小*/
char *rio_bufptr; /* 指向缓冲区未读数据的指针*/
char rio_buf[RIO_BUFSIZE]; /* 缓冲区*/
} rio_t;
RIO的使用示例
将文本文件的行从标准输入复制到标准输出
#include "csapp.h"
int main(int argc, char **argv) {
int n;
rio_t rio;
char buf[MAXLINE];
rio_readinitb(&rio, STDIN_FILENO);
while((n = rio_readlineb(&rio, buf, MAXLINE)) != 0)
rio_writen(STDOUT_FILENO, buf, n);
exit(0);
}
元数据、共享和重定向
文件元数据
元数据是关于数据的数据,在本节中特指文件的数据信息
每个文件的元数据都有内核维护
用户可以使用stat和fstat函数访问
#include <unistd.h>
#include <sys/stat.h>
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
//返回:若成功则为0,若出错则为-1
stat 函数以一个文件名(路径)作为输入,fstat函数以文件描述符作为输入
/* stat和fstat函数返回的元数据*/
struct stat {
dev_t st_dev; /* 设备*/
ino_t st_ino; /* inode */
mode_t st_mode; /* 保护模式和文件类型*/
nlink_t st_nlink; /* 硬链接数*/
uid_t st_uid; /* 拥有者ID */
gid_t st_gid; /* 拥有者所在组ID */
dev_t st_rdev; /* 设备类型(如果是inode设备) */
off_t st_size; /* 总大小(字节数) 这个很有用*/
unsigned long st_blksize; /* 文件系统I/O块大小*/
unsigned long st_blocks; /* 已分配的块数*/
time_t st_atime; /* 最后一次访问时间*/
time_t st_mtime; /* 最后一次修改时间*/
time_t st_ctime; /* inode的改变时间*/
};
Linux在 sys/stat.h 中定义了宏谓词来确定 st_mode成员 的文件类型:
- S_ISREG(m) 这是一个普通文件吗?
- S_ISDIR(m) 这是一个目录文件吗?
- S_ISSOCK(m) 这是一个网络套接字吗?
(注:这些系统调用man手册学习 man stat发现不是我们需要的,于是查看第二页man 2 stat)
在内核中如何表示已打开的文件
OS如何管理进程打开的文件?
两个描述符引用两个不同的打开文件
描述符1(stdout)指向终端,描述符4指向打开的磁盘文件

打开文件表是运行时的一些信息,pos表示游标,refcnt表示打开了几次(几个进程打开它)
描述符表为上一章task_struct中的一个表项
文件共享
两个不同描述符共享同一个磁盘文件
通过两个不同的打开文件表项
例如:使用相同的文件名参数两次调用open

不以共享的方式打开,这样游标就相互独立
进程间共享文件
子进程继承其父进程的打开文件
注意:通过exec函数不会改变这种情况(可以使用fcntl来更改)(原来打开的文件不会自动关闭)


注:这张图片的refcnt都为2
I/O重定向
在Shell中如何实现I/O重定向?
linux> ls > foo.txt
这样,就将stdout重定位成了foo.txt,这样ls本来输出到屏幕,但是重定向后,输出到该文件。
在代码里面,通过系统调用进行重定向
通过调用dup2(oldfd, newfd)函数,将(每个进程的)描述符表条目oldfd复制到条目newfd

I/O重定向示例
首先通过fork创建子进程,然后直接用dup2进行重定位,之后再execve
读取目录内容
应用程序可以用readdir系列函数来读取目录的内容
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
//返回:若成功,则为处理的指针;若出错,则为 NULLÿ
opendir以路径名为参数,返回指向目录流(directory stream)的指针。流是对 条目有序列表的抽象,在这里是指目录项的列表。
struct dirent *readdir(DIR *dirp);
//(返回:若成功,则为指向下一个目录项的指针;若没有更多的目录项或出错,则为 NULL)
每个目录项都是一个结构:
struct dirent {
ino_t d_ino; /* inode number */
char d_name[256]; /* Filename */
}
成员d_name 是文件名,d_ino是文件位置
唯一能区分错误和流结束情况的方法是检査自调用readdir以来errno是否被修改过
int closedir(DIR *dirp);
//返回:成功为0; 错误为-1
函数closedir关闭流并释放其所有的资源。
标准I/O
C标准库(libc.so)包含一系列更高级别的标准I/O函数(和RIO差不多,都是对unix IO的封装)
标准I/O函数包括:
- 文件的打开和关闭(fopen和fclose)
- 基于字节的读取和写入(fread和fwrite)
- 文本行的读取和写入(fgets和fputs)
- 格式化的读取和写入(fscanf和fprintf)
标准I/O将文件打开为流,这是文件描述符和内存中的缓冲区的抽象
C程序在开始时具有三个打开的流(在stdio.h中定义):
- stdin(标准输入)
- stdout(标准输出)
- stderr(标准错误)
#include <stdio.h>
extern FILE *stdin; /* standard input (descriptor 0) */
extern FILE *stdout; /* standard output (descriptor 1) */
extern FILE *stderr; /* standard error (descriptor 2) */
int main() {
fprintf(stdout, "Hello, world\n");
}
有缓冲的I/O:设计目标
应用程序通常以逐个字符的方式进行读写
getc、putc和ungetc
gets 和fgets (这些函数逐个字符地读取文本行,在遇到换行符时停止)
使用Unix I/O调用会降低性能
因为read 和write 需要进行系统调用,> 10,000个时钟周期
解决方案:使用缓冲读取
利用Unix read 函数来抓取字节块,用户输入函数从缓冲区逐个字节获取数据,当缓冲区为空时重新填充缓冲区。
标准I/O中的缓冲区
标准I/O使用带缓冲区的I/O
在遇到“\n”换行符时、调用fflush函数、缓冲区满、调用exit 函数、或从main 函数返回时, 缓冲区才被写入到文件中。
printf, fprintf等等
stdout与stderr的区别
stderr为无缓冲输出
stdout为有缓冲输出
标准I/O缓冲区的工作方式
可以使用Linux 中的strace程序来查看这种缓冲机制的实际运行情况。
linux> strace ./hello
execve("./hello", ["hello"], [/* ... */]).
...
write(1, "hello\n", 6) = 6
...
exit_group(0) = ?
hello.c :

printf()函数的调用过程


如何选择I/O函数
如图,unix IO是最底层的

Unix I/O 的优缺点
优点:
- Unix I/O 是最通用且开销最低的I/O 形式。
- 所有其他I/O 包都是使用Unix I/O 函数实现的。
- Unix I/O 提供了访问文件元数据的函数。
- Unix I/O 函数是异步信号安全的,可以在信号处理程序中安全使用。
缺点:
- 处理短计数(short counts)比较棘手且容易出错
- 高效地读取文本行需要一定形式的缓冲,这也是比较棘手且容易出错的。
- 这两个问题都可以通过标准I/O和RIO包来解决。
标准I/O 的优缺点
优点:
- 使用缓冲区提高了效率,通过减少read和write调用的次数。
- 短计数会被自动处理。
缺点:
不提供访问文件元数据的函数。
标准I/O函数不是异步信号安全的,并且不适用于信号处理程序。
标准I/O 不适用于网络套接字的输入和输出。
如何选择I/O函数
一般原则:尽可能使用最高级别的I/O函数
许多C程序员能够完全使用标准I/O函数完成所有工作
但是,请确保你理解所使用的函数!
何时使用标准I/O:当与磁盘或终端文件一起工作时。
何时使用Unix I/O:在信号处理程序中,因为Unix I/O是异步信号安全的。在极少数需要绝对最高性能的情况下
何时使用RIO:当您需要读写网络套接字时。避免在套接字上使用标准I/O
注:这三个不要同时在一个程序使用。
下面是RIO包的实现:
/*********************************************************************
* The Rio package - robust I/O functions
**********************************************************************/
/*
* rio_readn - robustly read n bytes (unbuffered)
*/
/* $begin rio_readn */
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 read() */
}
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* return >= 0 */
}
/* $end rio_readn */
/*
* rio_writen - robustly write n bytes (unbuffered)
*/
/* $begin 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 */
nwritten = 0; /* and call write() again */
else
return -1; /* errorno set by write() */
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
/* $end rio_writen */
/*
* rio_read - This is a wrapper for the Unix read() function that
* transfers min(n, rio_cnt) bytes from an internal buffer to a user
* buffer, where n is the number of bytes requested by the user and
* rio_cnt is the number of unread bytes in the internal buffer. On
* entry, rio_read() refills the internal buffer via a call to
* read() if the internal buffer is empty.
*/
/* $begin rio_read */
static 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 (errno != 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_bufptr, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}
/* $end rio_read */
/*
* rio_readinitb - Associate a descriptor with a read buffer and reset buffer
*/
/* $begin rio_readinitb */
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
/* $end rio_readinitb */
/*
* rio_readnb - Robustly read n bytes (buffered)
*/
/* $begin rio_readnb */
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) {
if (errno == EINTR) /* interrupted by sig handler return */
nread = 0; /* call read() again */
else
return -1; /* errno set by read() */
}
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* return >= 0 */
}
/* $end rio_readnb */
/*
* rio_readlineb - robustly read a text line (buffered)
*/
/* $begin rio_readlineb */
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')
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 = 0;
return n;
}
/* $end rio_readlineb */
/**********************************
* Wrappers for robust I/O routines
**********************************/
ssize_t Rio_readn(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
if ((n = rio_readn(fd, ptr, nbytes)) < 0)
unix_error("Rio_readn error");
return n;
}
void Rio_writen(int fd, void *usrbuf, size_t n)
{
if (rio_writen(fd, usrbuf, n) != n)
unix_error("Rio_writen error");
}
void Rio_readinitb(rio_t *rp, int fd)
{
rio_readinitb(rp, fd);
}
ssize_t Rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
ssize_t rc;
if ((rc = rio_readnb(rp, usrbuf, n)) < 0)
unix_error("Rio_readnb error");
return rc;
}
ssize_t Rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
ssize_t rc;
if ((rc = rio_readlineb(rp, usrbuf, maxlen)) < 0)
unix_error("Rio_readlineb error");
return rc;
}
/**************************
* Error-handling functions
**************************/
/* $begin errorfuns */
/* $begin unixerror */
void unix_error(char *msg) /* unix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
/* $end unixerror */
void posix_error(int code, char *msg) /* posix-style error */
{
fprintf(stderr, "%s: %s\n", msg, strerror(code));
exit(0);
}
void dns_error(char *msg) /* dns-style error */
{
fprintf(stderr, "%s: DNS error %d\n", msg, h_errno);
exit(0);
}
void app_error(char *msg) /* application error */
{
fprintf(stderr, "%s\n", msg);
exit(0);
}
/* $end errorfuns */
需要的结构:
/* Persistent state for the robust I/O (Rio) package */
/* $begin rio_t */
#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;
/* $end rio_t */