系统级I/O

一个Unix文件就是一个m个字节的序列:B0,B1,B(\k),B(\m-1)

所有的I/O设备,如网络、磁盘和终端,都被模型化为文件,而所有的输入和输出都被当做对相应的文件的读和写来执行。这种将设备优雅的映射为文件的方式,允许Unix内核引出一个简单的、低级的应用接口,称为Unix I/O

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标示这个文件。
  • 改变当前文件的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。
  • 读写文件:一个读操作就是从文件拷贝n>0个字节到存储器。写操作就是从存储器拷贝n>0个字节到一个文件。
  • 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的存储器资源。

打开和关闭文件

  • 进程通过调用open函数来打开一个已存在的文件或者创建一个新文件:

    #include <sys/type.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    int open(char* filename,int flag, mode_t mode);
    返回:成功则为新文件描述符,若出错为-1
    

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在晋城中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:可读可写

    fd = open("foo.txt",O_RDONLY,0);
    

flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:

  • O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件
  • O_TRUNC:如果文件已经存在,就截断它
  • O_APPEND:在每次写操作前,设置文件位置到文件的结尾处

    fd = open("foo.txt",O_ WRONLY | O_APPEND,0)
    打开一个已存在的文件,并在后面添加一些数据
    

mode参数指定了新文件的访问权限位,当进程通过带某个mode参数的open函数调用来创建一个文件时,文件的访问权限位被设置为mode &(~umask) 。给定下面的mode和umask默认值

    #define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
    #define DEF_UMASK S_IWGRP|S_IWOTH

创建一个新文件,文件的拥有者有读写权限,而所有其他的用户都有读权限

umask(DEF_UMASK);
fd = open("foo.txt",O_CREAT|O_TRUNC|O_WRONLY);

最后,进程通过调用close函数关闭一个打开的文件

    #include <unistd.h>
    int close(int fd);

关闭一个已关闭的描述符会出错

读和写文件

  • 应用程序分别调用read和write函数来执行输入和输出的

    #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
    
  • read函数从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

  • write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置

使用read和write调用一次一个字节地从标准输入拷贝到标准输出

#include "csapp.h"

int main(void)
{
    char c;
    while(read(STDIN_FILENO,&c,1)!=0)
    {
        write(STDOUT_FILENO,&c,1);
    }
    exit(0);
}

size_ t 被定义为unsigned int, 而ssize_t(有符号的大小)被定义为int。read函数返回一个有符号的大小,而不是一个无符号的大小,这是因为出错它必须返回-1。

在某些情况下,read和write传送的字节比应用程序要求要少,这些不足值不表示有错误:

  • 读时遇到EOF。假设我们准备读一个文件,该文件从当前文件位置开始只含有20多个字节,而我们以50字节的片读取。
  • 从终端读文本行。如果打开文件是与终端相关联的(如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小
  • 读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。对Unix管道调用read和write时,也有可能出现不足值。

用RIO包健壮地读写

  • (RIO)它会自动为你处理上文中所属的不足值。在像网络程序这样容易出现不足值的应用中,RIO包提供了方便健壮和高效的I/O。RIO提供了两类不同的函数:
    • 无缓冲的输入输出函数。这些函数直接在存储器和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
    • 带缓冲的输入函数。这些函数允许你高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于像printf这样的标准I/O函数提供的缓冲区。

RIO的无缓冲的输入输出函数

  • 通过调用rio_ readn和rio_writen函数,应用程序可以在存储器和文件之间直接传送数据。

    #include "csapp.h"
    ssize_ t rio_ readn(int fd,void* usr_buf,size _t n);
    ssize_ t rio_ writen(int fd,void* usr_buf,size _t n);
    返回:成功则为传送的字节数。若EOF则为0(只对rio_readn而言),若出错则为-1
    

rio_readn函数从描述符fd的当前文件位置最多传送n个字节到存储器位置usrbuf。类似的,rio _writen函数从位置usrbuf传送n个字节到描述符fd。rio _readn函数在遇到EOF时只能返回一个不足值。rio _writen函数绝不会返回不足值,对同一个描述符,可以任意交错地调用rio _readn和rio _writen。

RIO的带缓冲的输入函数

  • 一个文本行就是一个由换行符结尾的ASCII码字符序列。在Unix系统中,换行符(’ \n ‘)与ASCII换行符相同,数字值为0x0a。假设我们要编写一个程序来计算文件中文本行的数量该如何实现呢?一种方法就是用read函数一次一个字节地从文件传送到用户存储器,查找每个字节来查找换行符。缺点是效率不是很高,每读取文件中的一个字节都要求陷入内核。
  • 一个更好的方法是调用一个包装函数(rio_readlineb),它从一个内部读缓冲区拷贝一个文本行,当缓冲区变空时,会自动地调用read重新填满缓冲区。对于既包含文本行也包含二进制数据地文件,我们也提供了一个rio _readn带缓冲区地版本,叫做rio _readnb,它从和rio _readlineb一样地读缓冲区中传送原始字节。

    #include "csapp"
    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);
    返回:若成功则为读的字节数,若EOF则为0,若出错则为-1
    
    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)
                    nread = 0;
                else
                    return -1;
            }
            else if(nread == 0)
            {
                break;
            }
            nleft -= nread;
            bufp += nread;
        }
        return (n-nleft);
    }
    
    ssize_t rio_writen(int fd, void *usrbuf,size_t n)
    {
        size_t nleft = n;
        ssize_t nwriteen;
        char *bufp = usrbuf;
        while(nleft >0)
        {
              if((nwriteen = write(fd, bufp, nleft))<=0)
              {
                    if(errno == EINTR)
                        nwritten = 0;
                    else
                        return -1;
                }
                nleft -= nwritten;
                bufp += nwritten;
        }
    }
    

每打开一个描述符都会调用一次rio_readinitb函数。它将描述符fd和地址rp处的一个类型为rio_t的读缓冲区联系起来。

rio_readinitb函数从文件rp读出一个文本行(包括结尾的换行符),将它拷贝到存储器位置usrbuf,并且用空(零)来结束这个文本行。rio_readlineb函数最多读maxlen-1个字节,余下的一个字符留给结尾的空字符。超过maxlen-1字节的文本行被截断,并用一个空字符结束。

rio_readnb函数从文件rp最多读n个字节到存储器位置usrbuf。对同一个描述符,对rio_readlineb和rio_readnb的调用可以任意交叉进行。然而,对这些带缓冲区的函数的调用却不应和无缓冲的rio_readn函数交叉使用。

如何使用RIO函数来一次一行地从标准输入拷贝一个文本文件到标准输出。

#include "csapp.h"
int main()
{
    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);        
    }   
}

读一个缓冲区,以及初始化它的rio_readinitb函数的代码,rio_readinitb函数创建了一个空的读缓冲区,并且将一个打开的文件描述符和这个缓冲区联系起来。

    #define RIO_BUFSIZE 8192
    typedef struct {
        int rio_fd;
        int rio_cnt;
        char *rio_bufptr;
        char rio_buf[RIO_BUFSIZE];
    }rio_t;

    void rio_readinitb(rio_t *rp,int fd)
    {
        rp->rio_fd = fd;
        rp->rio_cnt = 0;
        rp->rio_bufptr = rp->rio_buf;   
    }

RIO读程序的核心是rio_read函数。rio_read函数是Unix read函数的带缓冲的版本。当调用rio_read要求读n个字节时,读缓冲区内有rp->rio_cnt个未读字节。如果缓冲区为空,那么会通过调用read再填满它。这个read调用收到一个不足值并不是错误,只不过读缓冲区是填充了一部分。一旦缓冲区非空,rio_read就从读缓冲区拷贝n和rp->rio_cnt中较小值到用户缓冲区,并返回拷贝的字节数。

对于一个应用程序,rio_read函数和Unix read函数有同样的语义。再出错时,返回-1,并且适当的设置errno。在EOF时,它返回值0。如果要求的字节数超过了读缓冲区内未读的字节的数量,它会返回一个不足值。

应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(也称为文件的元数据)。

#include <unistd.h>
#include <sys/stat.h>

int stat(const char *filename,struct stat *buf);
int fstat(int fd,stat *buf);
返回:若成功则为0,若出错则为-1

stat函数以一个文件名作为输入,并填写下面所示的一个stat数据结构中的各个成员。fstat函数是相似的,只不过是以文件描述符而不是文件名作为输入。

    struct stat {
        mode_t st_mode;
        ...
        off_t st_size;
        ...
        };

st_size成员包含了文件的字节数大小。st_mode成员则编码了文件访问许可位和文件类型。Unix识别大量不同的文件类型。普通文件包括某种类型的二进制或文本数据。对于内核而言,文本文件和二进制文件毫无区别。目录文件包含关于其他文件的信息。套接字是一种用来通过网络与其他进程通信的文件。

Unix提供的宏指令根据st_mode成员来确定文件的类型。

S_ISREG()      这是一个普通文件吗?
S_ISDIR()      这是一个目录文件吗?
S_ISSOCK()     这是一个网络套接字吗?

如何使用这些宏和stat函数来读取和解释一个文件的st_mode位。

    #include "csapp.h"
    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);    
    }
    查询和处理一个文件的st_mode位

共享文件

  • 可以用许多不同的方式来共享Unix文件。内核用三个相关的数据结构来表示打开的文件:

    • 描述符表:每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
    • 文件表:打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括有当前的文件位置、引用计数(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
    • v-node表:同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
  • 描述符1和4通过不同的打开文件表表项来引用两个不同的文件。这是一种典型的情况,没有共享文件,并且每个描述符对应一个不同的文件这里写图片描述

  • 多个描述符也可以通过不同的文件表表项来引用同一个文件。这里写图片描述
  • 父子进程如何共享文件。假设fork之前,父进程有图一所示的打开文件。然后,下图是调用fork后的情况。子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合。因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。

I/O重定向

  • Unix外壳提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。

    unix > ls > foo.txt
    

I/O重定向是如何工作的呢?一种方式是使用dup2函数。

    #include <unistd.h>
    int dup2(int oldfd,int new fd);
    返回:若成功则为非负的描述符,若出错则为-1

dup2函数拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在拷贝oldfd之前关闭newfd。这里写图片描述

假设在调用dup2(4,1)之前,我们的状态如上图一所示,其中描述符1(标准输出)对应于文件A(比如一个终端),描述符4对应于文件B(比如一个磁盘文件)。A和B的引用计数都等于1,上图显示了调用dup2(4,1)之后的情况。两个描述符现在都指向文件B;文件A已经关闭了,并且它的文件表和v-node表也已经被删除了。文件B的引用计数已经增加了。从此以后,任何写到标准输出的数据都被重定向到文件B。

标准I/O

  • ANSI C定义了一组高级输入输出函数,称为标准I/O库,为程序员提供了Unix I/O的较高级别的替代。这个库(libc)提供了打开和关闭的函数(fopen和fclose)、读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs),以及复杂的格式化的I/O函数(scanf和printf)。
  • 标准I/O库将一个打开的文件模型化为一个流,对于程序员而言,一个流就是一个指向FILE类型的结构的指针。每个ANSI C程序开始时都有三个打开的流stdin、stdout和stderr,分别对应于标准输入、标准输出和标准错误:

    #include <stdio.h>
    extern FILE *stdin;
    extern FILE *stdout;
    extern FILE *stderr;
    

    类型为FILE的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的Unix I/O系统调用的数量尽可能的小。例如,假设我们有一个程序,它反复调用标准I/O的getc函数,每次调用返回文件的下一个字符。当第一次调用getc时,库调用一次read函数来填充流缓冲区,然后将缓冲区中的第一个字节返回给应用程序。只要缓冲区中还有未读的字节,接下来对getc的调用就能直接从流缓冲区得到服务。

综合:我们该使用哪些I/O函数

  • Unix I/O是在操作系统内核中实现的。应用程序可以通过open、close、lseek、read、write和stat这样的函数来访问Unix I/O。较高级别的RIO和标准I/O函数都是基于Unix I/O函数来实现的。RIO函数是read和write的健壮的包装函数。它们自动处理不足值,并且为读文本行提供了一种高效的带缓冲的方法。标准I/O函数提供了Unix I/O函数的一个更加完整的带缓冲的替代品,包括格式化的I/O例程。

这里写图片描述

  • 标准I/O函数是磁盘和终端设备I/O之选。
  • 我们试图对网络输入输出使用标准I/O时,它却带来了一些令人讨厌的问题。Unix对网络的抽象是一种称为套接字的文件类型。和任何Unix文件一样,套接字也是用文件描述符来引用的。称为套接字描述符。应用程序通过读写套接字描述符来与运行在其他计算机上的进程通信。
  • 标准I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。然而对流的限制和对套接字的限制,有时候会互相冲突,而又极少有文档描述这些现象。
    • 限制一:跟在输出函数之后的输入函数。如果中间没有插入对fflush、fseek、fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush函数清空与流相关的缓冲区。后三个函数使用Unix I/O lseek函数来重置当前的文件位置。
    • 限制二:跟在输入函数的输出函数。如果中间没有插入对fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个EOF。

这些限制给网络应用带来了一个问题,因为对套接字使用lseek函数是非法的。对流I/O的第一个限制能够通过采用在每个输入操作前刷新缓冲区这样的规则来满足。然而,要满足第二个限制的唯一办法是,对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写:

    FILE *fpin, *fpout;

    fpin = fdopen(sockfd,"r");
    fpout = fdopen(scokfd,"w");

它要求应用程序在两个流上都要调用fclose,这样才能释放与每个流相关联的存储器资源,避免存储器泄露。

    fclose(fpin);
    flcose(fpout);

这些操作中的每一个都试图关闭同一个底层的套接字描述符,所以第二个close操作就会失败。对于顺序的程序来说,这并不是问题,但是在一个线程化的程序中关闭一个已经关闭了的描述符是会导致灾难的。

因此,建议在网络套接字上不要使用标准I/O函数来进行输入和输出。而要使用健壮的RIO函数。如果你需要格式化的输出,使用sprintf函数在存储器中格式化一个字符串,然后用rio_writen把它发送到套接字。如果你需要格式化输入,使用rio_readlineb来读一个完整的文本行,然后用sscanf从文本行提取不同的字段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值