0. 学习起因
大多时候,高级别I/O函数工作良好,没必要间接用Unix I/O,为何需学习?
- 理解Unix I/O将帮忙了解其余零碎概念。I/O是零碎操作不可或缺的局部,因而常常遇到I/O和其余零碎概念之间的循环依赖
- 有时必须用Unix I/O,用高级I/O不太可能或不适合,如规范I/O库没提供读取文件元数据的形式,此外I/O库存在一些问题
1. Unix I/O
输出/输入(I/O)是主存和外部设备之间复制数据的过程,在 Linux 中,文件就是字节的序列。所有的 I/O 设施(如网络、内核、磁盘和终端等)都被模型化为文件,而所有的输出和输入都被当作对相应文件的读和写来执行。这种将设施映射为文件的机制,容许内核引出简略、优雅的利用接口Unix I/O,使得所有输出和输入都以对立的形式执行,如 open()
/close()
关上/敞开文件,read()
/ write()
读/写文件。seek()
扭转以后文件地位。Unix I/O次要分为两大类:
为辨别不同文件的类型,会有一个 type
来进行区别:
- 一般文件:蕴含任意数据
- 目录:相干文件组的索引
- Socket:用于另一台机器上的过程通信
还有一些特地的类型仅做理解:命名管道(FIFOs)、符号链接、字符和块设施
一般文件
一般文件蕴含任意数据,应用程序通常需辨别文本文件和二进制文件。前者只蕴含 ASCII 或 Unicode 字符。除此之外的都是二进制文件(对象文件, JPEG 图片, 等等)。内核不能辨别出区别。
文本文件是文本行的序列,每行以 \n
结尾,新行是 0xa
,和 ASCII 码中LF一样。不同零碎判断行完结的符号不同(End of line, EOL),Linux & Mac OS是\n
(0xa)等价line feed(LF),而Windows & 网络协议是\r\n
(0xd 0xa)等价Carriage return(CR) followed by line feed(LF)
目录
目录蕴含一个链接(link)数组,且每个目录至多蕴含两记录:.
(dot) 当前目录、..
(dot dot) 下层目录
操作命令次要有 mkdir
, ls
, rmdir
。目录以树状构造组织,根目录是 /
(slash)。
内核会为每个过程保留当前工作目录(cwd, current working directory),可用 cd
命令进行更改。通过路径名来确定文件的地位,分为绝对路径和相对路径。
2. 文件操作
2.1 关上文件
关上文件会告诉内核已筹备好拜访该文件
int fd; // 文件描述符 file descriptorif ((fd = open("/etc/hosts", O_RDONLY)) < 0){ perror("open"); exit(1);}
返回值是一个小的整型称为文件描述符(file descriptor),若该值等于 -1 则阐明产生谬误。每个由 Linux shell创立的过程都会默认关上三个文件(留神这里的文件概念):
- 0: standard input(stdin)
- 1: standard output(stdout)
- 2: standar error(stderr)
2.2 敞开文件
敞开文件会告诉内核已实现对该文件的拜访
int fd; // 文件描述符int retval; // 返回值int ((retval = close(fd)) < 0){ perror("close"); exit(1);}
敞开一个曾经敞开的文件是线程程序中的劫难(稍后会具体介绍),所以肯定要查看返回值,哪怕是看似良好的函数如 close()
2.3 读取文件
读取文件将字节从以后文件地位复制到内存,而后更新文件地位
char buf[512];int fd;int nbytes // 关上文件描述符,并从中读取 512 字节的数据if ((nbytes = read(fd, buf, sizeof(buf))) < 0){ perror("read"); exit(1);}
返回值是读取的字节数量,是一个 ssize_t
类型(其实就是一个有符号整型),如果 nbytes < 0
那么示意出错。nbytes < sizeof(buf)
这种状况(short counts) 是可能产生的,而且并不是谬误。
2.4 写入文件
写入文件将字节从内存复制到以后文件地位,而后更新以后文件地位
char buf[512];int fd;int nbytes;// 关上文件描述符,并向其写入 512 字节的数据if ((nbytes = write(fd, buf, sizeof(buf)) < 0){ perror("write"); exit(1);}
返回值是写入的字节数量,如果 nbytes < 0
示意出错。nbytes < sizeof(buf)
这种状况(short counts) 是可能产生的,且不是谬误。
2.5 读取目录
可用readdir系列函数读取目录的内容,每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或没有更多目录项则返回NULL,每个目录项都有构造体:
struct dirent{ ino_t d_ino; /* inode number */ char d_name[256]; /* filename */};
2.6 简略Unix I/O 例子
拷贝文件到规范输入,一次一个字节:
#include "csapp.h"int main(int argc, char *argv[]){ char c; int infd = STDIN_FILENO; if (argc == 2) { infd = Open(argv[1], O_RDONLY, 0); } while(Read(infd, &c, 1) != 0) Write(STDOUT_FILENO, &c, 1); exit(0);}
后面提到的 short count 会在上面的情景下产生:
- 读取的时遇到 EOF(end-of-file)
- 从终端中读取文本行
- 读和写网络 sockets
但上面的状况下不会产生
- 从磁盘文件中读取(除 EOF 外)
- 写入到磁盘文件中
最好总是容许 short count,这样就能够防止解决这么多不同的状况。
#include "csapp.h"#define BUFSIZE 64int main(int argc, char *argv[]){ char buf[BUFSIZE]; int infd = STDIN_FILENO; if (argc == 2) { infd = Open(argv[1], O_RDONLY, 0); } while((nread = Read(infd, buf, BUFSIZE)) != 0) Write(STDOUT_FILENO, buf, nread); exit(0);}
3. 元数据
元数据是用来形容数据的数据,由内核保护,能够通过 stat
和 fstat
函数来拜访,构造是:
struct stat{ dev_t st_dev; // Device ino_t st_ino; // inode mode_t st_mode; // Protection & file type nlink_t st_nlink; // Number of hard links uid_t st_uid; // User ID of owner gid_t st_gid; // Group ID of owner dev_t st_rdev; // Device type (if inode device) off_t st_size; // Total size, in bytes unsigned long st_blksize; // Blocksize for filesystem I/O unsigned long st_blocks; // Number of blocks allocated time_t st_atime; // Time of last access time_t st_mtime; // Time of last modification time_t st_ctime; // Time of last change}
对应的拜访例子:
int main (int argc, char **argv){ struct stat stat; char *type, *readok; Stat(argv[1], &stat); if (S_ISREG(stat.st_mode)) // 确定文件类型 type = "regular"; else if (S_ISDIR(stat.st_mode)) type = "directory"; else type = "other"; if ((stat.st_mode & S_IRUSR)) // 查看读权限 readok = "yes"; else readok = "no"; printf("type: %s, read: %s\n", type, readok); exit(0);}
3.1 共享文件
可用许多不同的形式共享Linux文件。要了解文件共享就得了解三个示意关上文件的数据结构。
- 描述符表:每个过程都有独立的描述符表,表项是过程关上的文件描述符索引,每个关上的描述符表指向文件表中的一个表项
- 文件表:关上文件的汇合是由一张文件表示意,所有过程共享该表,每个文件表的表项组成包含以后文件地位、援用计数、指向v-node表中对应表项的指针。敞开描述符缩小文件表项援用计数直到为0会删除文件表项
- v-node表:同文件表一样所有过程共享该v-node表,每个表蕴含stat构造中的大多数信息,包含st_mode和st_size成员
两个描述符援用两个不同的关上文件。描述符 1(stdout)指向终端,描述符 4 指向关上的磁盘文件
两个不同的描述符通过两个不同的关上文件表条目共享同一个磁盘文件,应用雷同的文件名参数调用 open 两次,要害思维是每个描述符都有本人的文件地位,所以对不同描述符的读操作可从文件不同地位获取数据
3.2 过程如何共享文件:fork
子过程继承其父过程的关上文件,留神:exec 函数不会扭转状况(应用 fcntl 来扭转)
在 fork 之后,子过程实际上和父过程的指向是一样的,这里须要留神的是会把援用计数加 1
4. I/O重定向
理解了这个,就晓得重定向如何实现。其实很简略,只有调用 dup2(oldfd, newfd)
函数即可。扭转文件描述符指向的文件,就实现重定向,下图中咱们把原来指向终端的文件描述符指向了磁盘文件,也就把终端上的输入保留在了文件中:
具体来说:
关上规范输入应该重定向到的文件,产生在执行 shell 代码的子过程中,在 exec 之前
调用
dup2(4,1)
,导致fd=1 (stdout) 援用 fd=4 指向的磁盘文件,留神援用计数的变动
4.1 I/O重定向的例子
4.2 过程管制和I/O
5. 规范I/O
C规范库中蕴含一系列高层的规范 IO 函数,比方
- 关上和敞开文件:
fopen
,fclose
- 读取和写入字节:
fread
,fwrite
- 读取和写入行:
fgets
,fputs
- 格式化读取和写入:
fscanf
,fprintf
规范 IO 会用流的模式关上文件,所谓流(stream)理论是文件描述符和缓冲区(buffer)在内存中的形象。C 程序个别以三个流开始,如下所示:
#include <stdio.h>extern FILE *stdin; // 规范输出 descriptor 0extern FILE *stdout; // 规范输入 descriptor 1extern FILE *stderr; // 规范谬误 descriptor 2int main(){ fprintf(stdout, "Hello, Da Wang\n");}
用缓冲区的起因,程序常常会一次读/写一个字符,比方 getc
, putc
, ungetc
,同时也会一次读/写一行,比方 gets
, fgets
。如果用 Unix I/O 的形式来进行调用是十分低廉的, read
和 write
因需内核调用,需大于10000 个时钟周期。
方法是用 read
一次读取一块数据,再由高层的用户输出函数,一次从缓冲区读取一个字符(当缓冲区用完的时候须要从新填充)
5.1 规范I/O中的缓冲
规范I/O函数应用缓冲的I/O,缓冲区通过\n
刷新到输入fd,调用fflush或exit,或从main返回
可通过strace程序看缓冲如何起作用
6. RIO
RIO和C准库IO库是在Unix I/O上构建的两个不兼容的库,RIO提供了不便高效的IO拜访,能够从一个描述符中读二进制非缓存输出和输入、缓存的文本行和二进制数据(线程平安可在同一描述符上任意穿插应用)。有两类输入输出函数:
① 无缓冲输入输出:和Unix的read/write接口雷同,对网络传输数据尤其有用
对于同一个形容表,能够任意的交织调用rio_readn和rio_wiriten
/** rio_readn - Robustly read n bytes (unbuffered)*/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 */}
从下面的代码不难看出,如果程序的信号处理程序返回中断,这个函数会手动重启read或者write。
② 带缓冲的输出函数
这个函数有一个益处是,它从外部读缓冲区拷贝的一行,能无效地从局部缓存在外部内存缓冲区中的文件中读取文本行和二进制数据,当缓冲区为空的时候,主动调用read填满缓冲区,效率很高。
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
,rio_readlineb从文件fd中读取最多maxlen字节的文本行,并将其存储在usrbuf中,当读到maxlen字节、EOF产生、\n
产生时进行。
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
,rio_readnb从文件fd读取最多n个字节,当读到n个字节、EOF产生时进行,对rio_readlineb和rio_readnb的调用能够在同一个描述符上任意交织
缓存IO实现
读文件时,文件有关联的缓冲区来保留曾经从文件中读取但还没有被用户代码读取的字节
缓存IO申明
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;
#include "csapp.h"#define MLINE 1024int main(int argc, char *argv[]){ rio_t rio; char buf[MLINE]; int infd = STDIN_FILENO; ssize_t nread = 0; if (argc == 2) { infd = Open(argv[1], O_RDONLY, 0); } Rio_readinitb(&rio, infd); while((nread = Rio_readlineb(&rio, buf, MLINE)) != 0) Rio_writen(STDOUT_FILENO, buf, nread); exit(0);}
复制文件到规范输入,用mmap加载整个文件
#include "csapp.h"int main(int argc, char **argv){ struct stat stat; if (argc != 2) exit(1); int infd = Open(argv[1], O_RDONLY, 0); Fstat(infd, &stat); size_t size = stat.st_size; char *bufp = Mmap(NULL, size, PROT_READ,MAP_PRIVATE, infd, 0); Write(1, bufp, size); exit(0);}
7. 总结
Unix I/O 是最底层的,通过零碎调用进行操作,C的规范I/O库建设在此之上,对应的函数为:
I/O | 长处 | 毛病 |
---|---|---|
Unix I/O | 最通用最底层的I/O办法,异步信号平安,即可在信号处理器中调用,提供拜访文件元数据的性能 | 底层和根底,故需解决的状况多且易错,高效率的读写需缓冲区,也易错,这是规范I/o要解决的问题 |
规范I/O | 缓冲通过缩小读和写零碎调用的数量来提高效率,短计数即取得的字符没达到SIZE的问题 | 不提供文件元数据拜访性能,规范I/O非异步信号平安,不适宜信号处理和网络套接字输出/出 |
抉择I/O函数
通用规定:尽量用高层的I/O函数,但了解用的函数。当用磁盘或终端文件时,用规范I/O,在信号处理程序外部,极少数须要最高性能时用Unix I/O
解决二进制文件
任意字节序列,蕴含0x00的字节的二进制文件,永远不要用面向文本的I/O,如fgets、scanf等,而应用rio_readn或rio_readnb,以及字符串函数,如strlen、strcpy、strcat等。
本文由博客一文多发平台 OpenWrite 公布!