大多时候,高级别I/O函数工作良好,没必要直接用UnixI/O,为何需学习?
了解UnixI/O将帮助理解其他系统概念。I/O是系统操作不可或缺的部分,因此经常遇到I/O和其他系统概念之间的循环依赖
有时必须用UnixI/O,用高级I/O不太可能或不合适,如标准I/O库没提供读取文件元数据的方式,此外I/O库存在一些问题
1.UnixI/O输入/输出(I/O)是主存和外部设备之间复制数据的过程,在Linux中,文件就是字节的序列。所有的I/O设备(如网络、内核、磁盘和终端等)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的机制,允许内核引出简单、优雅的应用接口UnixI/O,使得所有输入和输出都以统一的方式执行,如open()/close()打开/关闭文件,read()/write()读/写文件。seek()改变当前文件位置。UnixI/O主要分为两大类:
为区分不同文件的类型,会有一个type来进行区别:
普通文件:包含任意数据
目录:相关文件组的索引
Socket:用于另一台机器上的进程通信
还有一些特别的类型仅做了解:命名管道(FIFOs)、符号链接、字符和块设备
普通文件
普通文件包含任意数据,应用程序通常需区分文本文件和二进制文件。前者只包含ASCII或Unicode字符。除此之外的都是二进制文件(对象文件,JPEG图片,等等)。内核不能区分出区别。
文本文件是文本行的序列,每行以\n结尾,新行是0xa,和ASCII码中LF一样。不同系统判断行结束的符号不同(ofline,EOL),LinuxMacOS是\n(0xa)等价linefeed(LF),而Windows网络协议是\r\n(0xd0xa)等价Carriagereturn(CR)followedbylinefeed(LF)
目录
目录包含一个链接(link)数组,且每个目录至少包含两记录:.(dot)当前目录、..(dotdot)上层目录
操作命令主要有mkdir,ls,rmdir。目录以树状结构组织,根目录是/(slash)。
内核会为每个进程保存当前工作目录(cwd,currentworkingdirectory),可用cd命令进行更改。通过路径名来确定文件的位置,分为绝对路径和相对路径。
2.文件操作2.1打开文件打开文件会通知内核已准备好访问该文件
intfd;//文件描述符filedescriptorif((fd=open("/etc/hosts",O_RDONLY))0){perror("open");exit(1);}返回值是一个小的整型称为文件描述符(filedescriptor),若该值等于-1则说明发生错误。每个由Linuxshell创建的进程都会默认打开三个文件(注意这里的文件概念):
0:standardinput(stdin)
1:standardoutput(stdout)
2:standarerror(stderr)
2.2关闭文件关闭文件会通知内核已完成对该文件的访问
intfd;//文件描述符intretval;//返回值int((retval=close(fd))0){perror("close");exit(1);}关闭一个已经关闭的文件是线程程序中的灾难(稍后会详细介绍),所以一定要检查返回值,哪怕是看似良好的函数如close()
2.3读取文件读取文件将字节从当前文件位置复制到内存,然后更新文件位置
charbuf[512];intfd;intnbytes//打开文件描述符,并从中读取512字节的数据if((nbytes=read(fd,buf,sizeof(buf)))0){perror("read");exit(1);}返回值是读取的字节数量,是一个ssize_t类型(其实就是一个有符号整型),如果nbytes0那么表示出错。nbytessizeof(buf)这种情况(shortcounts)是可能发生的,而且并不是错误。
2.4写入文件写入文件将字节从内存复制到当前文件位置,然后更新当前文件位置
charbuf[512];intfd;intnbytes;//打开文件描述符,并向其写入512字节的数据if((nbytes=write(fd,buf,sizeof(buf))0){perror("write");exit(1);}返回值是写入的字节数量,如果nbytes0表示出错。nbytessizeof(buf)这种情况(shortcounts)是可能发生的,且不是错误。
2.5读取目录可用readdir系列函数读取目录的内容,每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或没有更多目录项则返回NULL,每个目录项都有结构体:
structdirent{ino_td_ino;/*inodenumber*/chard_name[256];/*filename*/};2.6简单UnixI/O例子拷贝文件到标准输出,一次一个字节:
include""*stdin;//标准输入descriptor0externFILE*stdout;//标准输出descriptor1externFILE*stderr;//标准错误descriptor2intmain(){fprintf(stdout,"Hello,DaWang\n");}用缓冲区的原因,程序经常会一次读/写一个字符,比如getc,putc,ungetc,同时也会一次读/写一行,比如gets,fgets。如果用UnixI/O的方式来进行调用是非常昂贵的,read和write因需内核调用,需大于10000个时钟周期。
办法是用read一次读取一块数据,再由高层的用户输入函数,一次从缓冲区读取一个字符(当缓冲区用完的时候需要重新填充)
5.1标准I/O中的缓冲标准I/O函数使用缓冲的I/O,缓冲区通过\n刷新到输出fd,调用fflush或exit,或从main返回
可通过strace程序看缓冲如何起作用
6.RIORIO和C准库IO库是在UnixI/O上构建的两个不兼容的库,RIO提供了方便高效的IO访问,可以从一个描述符中读二进制非缓存输入和输出、缓存的文本行和二进制数据(线程安全可在同一描述符上任意交叉使用)。有两类输入输出函数:
①无缓冲输入输出:和Unix的read/write接口相同,对网络传输数据尤其有用
对于同一个描述表,可以任意的交错调用rio_readn和rio_wiriten
/**rio_readn-Robustlyreadnbytes(unbuffered)*/ssize_trio_readn(intfd,void*usrbuf,size_tn){size_tnleft=n;ssize_tnread;char*bufp=usrbuf;while(nleft0){if((nread=read(fd,bufp,nleft))0){if(errno==EINTR)/*Interruptedbysighandlerreturn*/nread=0;/*andcallread()again*/elsereturn-1;/*errnosetbyread()*/}elseif(nread==0)break;/*EOF*/nleft-=nread;bufp+=nread;}return(n-nleft);/*Return=0*/}从上面的代码不难看出,如果程序的信号处理程序返回中断,这个函数会手动重启read或者write。
②带缓冲的输入函数
这个函数有一个好处是,它从内部读缓冲区拷贝的一行,能有效地从部分缓存在内部内存缓冲区中的文件中读取文本行和二进制数据,当缓冲区为空的时候,自动调用read填满缓冲区,效率很高。
ssize_trio_readlineb(rio_t*rp,void*usrbuf,size_tmaxlen);,rio_readlineb从文件fd中读取最多maxlen字节的文本行,并将其存储在usrbuf中,当读到maxlen字节、EOF发生、\n发生时停止。
ssize_trio_readnb(rio_t*rp,void*usrbuf,size_tn);,rio_readnb从文件fd读取最多n个字节,当读到n个字节、EOF发生时停止,对rio_readlineb和rio_readnb的调用可以在同一个描述符上任意交错
缓存IO实现
读文件时,文件有关联的缓冲区来保存已经从文件中读取但还没有被用户代码读取的字节
缓存IO声明
typedefstruct{intrio_fd;/*descriptorforthisinternalbuf*/intrio_cnt;/*unreadbytesininternalbuf*/char*rio_bufptr;/*nextunreadbyteininternalbuf*/charrio_buf[RIO_BUFSIZE];/*internalbuffer*/}rio_t;defineMLINE1024intmain(intargc,char*argv[]){rio_trio;charbuf[MLINE];intinfd=STDIN_FILENO;ssize_tnread=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加载整个文件
I/O
优点
缺点
UnixI/O
最通用最底层的I/O方法,异步信号安全,即可在信号处理器中调用,提供访问文件元数据的功能
底层和基础,故需处理的情况多且易错,高效率的读写需缓冲区,也易错,这是标准I/o要解决的问题
标准I/O
缓冲通过减少读和写系统调用的数量来提高效率,短计数即获得的字符没达到SIZE的问题
不提供文件元数据访问功能,标准I/O非异步信号安全,不适合信号处理和网络套接字输入/出
选择I/O函数
通用规则:尽量用高层的I/O函数,但理解用的函数。当用磁盘或终端文件时,用标准I/O,在信号处理程序内部,极少数需要最高性能时用UnixI/O
处理二进制文件
任意字节序列,包含0x00的字节的二进制文件,永远不要用面向文本的I/O,如fgets、scanf等,而使用rio_readn或rio_readnb,以及字符串函数,如strlen、strcpy、strcat等。





