CSAPP-----系统级IO

版权声明: https://blog.csdn.net/zl6481033/article/details/85840512

本章目录:

1、Unix I/O

2、文件

3、打开和关闭文件

4、读和写文件

5、用RIO包健壮的读写

6、读取文件元数据

7、读取目录内容

8、共享文件    

9、IO重定向

10、标准I/O

11、具体使用情况

12、小结 


本系列文章的观点和图片均来自《深入理解计算机系统第 3 版》仅作为学习使用

        输入输出(I/O)是主存和外部设备(磁盘、终端、网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,输出操作是从主存到I/O设备。所有语言的运行时系统都提供执行I/O的较高级别的工具,例如C中的printf和scanf带有缓冲区的I/O函数,C++中重载的<<(输入)和>>(输出),linux中使用内核提供的系统级I/O函数来实现较高级别的I/O函数。虽然高级别的I/O函数工作良好,但是理解UNIX 的I/O也是很有必要的。

        *了解Unix I/O可以帮助你理解其他系统概念。I/O是系统操作不可或缺的一部分。I/O和系统的其他概念之间都是相互依赖的,比如I/O在进程的创建和执行中扮演重要角色,反过来进程中间又在不同进程间文件共享扮演者关键角色。

        *有时候除了Unix别无选择。在某些时候,高级I/O函数不太合适,比如标准I/O库没有读取文件元数据的方式(文件大小、创建时间),另外I/O库还存在一些问题,使用它来网络编程非常冒险。

1、Unix I/O

        在linux中所有的I/O设备(网络、磁盘、终端)都被模型化为文件,所有的输入输出都当作相应文件的读写来执行,这种将设备映射为文件的方式,允许linux内核引出一个简单的、低级的接口称为Unix I/O,使得所有的输入和输出都能以一种统一并且一致的方式来执行。

        *打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负数,叫做描述符,它在后续对此文件的操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。

        *linux shell创建的每个进程开始都有三个打开的文件:标准输入(描述符为0),标准输出(描述符1)和标准错误输出(描述符2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,用来代替描述符值。

        *改变当前文件位置,对于每一个打开的文件,内核保持一个文件位置k,初试为0,这个文件位置就是文件从头开始的字节偏移量,应用程序可以通过执行seek操作显示的设置文件的当前位置为k。

      *读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m的字节文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。类似的,写操作就是从内存复制n个字节到一个文件。从当前位置k开始,然后更新k。

       *关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有的打开的文件,并释放他们的内存资源。

2、文件

        每个linux文件都有一个类型来表明它在系统中的角色。

        *普通文件,包含任意数据,应用程序常常要区分文本文件和二进制文件,文本文件只是含有ASCII或Unicode字符的普通文件,二进制文件是所有其他文件,对内核而言,这两种文件没有区别。linux文本文件包含一个文本行序列,其中一行是一个字符序列,以一个新行符(“\n”)结束,新行符和ASCII的换行符是一样的,其数字值为0x0a。

        *目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录,每个目录至少含有两个条目,“.”是该目录自身的链接,“..”是目录层次结构中父目录的链接。、

        *套接字是用来与另一个进程跨网络通信的文件。

        其他文件类型包含命名通道、符号链接、以及字符和块设备。

        linux内核将所有文件都组成一个目录层次结构,由名为/的根目录确定。系统中每个目录都是根目录的直接或间接的后代。如下图:
       

        作为上下文的一部分,每个进程都有一个当前的工作目录来确定其在目录层次结构的当前位置,可以用cd命令来修改shell中的当前目录。

        目录层次结构中位置用路径名来指定,路径名是一个字符串,包括一个可选斜杠,其后紧跟一系列的文件名,文件名之间用斜杠分隔,路径名有两种形式:

        *绝对路径名以一个斜杠开始,表示从根节点开始的路径。

        *相对路径名以文件名开始,表示从当前目录开始的路径。

3、打开和关闭文件

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

       open原型(https://blog.csdn.net/zl6481033/article/details/85285697#3%E3%80%81open%E5%87%BD%E6%95%B0

        

        open将filename转换成一个文件描述符,并返回描述符数字,返回的描述符总是在进程中没有达赖的最小描述符。flag参数指明进程打算如何访问这个文件。flag具体参数可参考上原型。

        mode参数制定了新文件的访问权限位,这些位的符号名字如下图所示。

       

        作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的,当进程通过某个带mode参数的open函数调用来创建一个新文件,文件的访问权限位被设置为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("a.txt",O_CREAT|O_TRUNC|O_WRONLY,DEF_MODE);

        最后,进程调用close函数关闭一个已打开文件。关闭一个已关闭的描述符会出错。

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

4、读和写文件

        应用程序通过调用read和write函数执行输入和输出。


#include <unistd.h>
 
ssize_t write(int fd, const void *buf, size_t count);

ssize_t read(int fd, void *buf, size_t count);

         read函数从描述符为fd的当前文件位置复制最多n个字节到内存buf,返回-1表示一个错误,返回0标识EOF。否则返回值标识实际传送的字节数量。

        write函数从内存buf位置复制至多n个字节到描述符fd的当前位置。通过调用lseek函数应用程序可以显式的修改当前文件的位置。

        ssize_t和size_t的区别:read有一个size_t的参数和ssize_t的返回值,在x86_64系统中,size_t被定义为unsigned long 而ssize_t被定义为long。read返回的是一个有符号的大小,出错会返回-1。

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

        *读时遇到EOF,假设要读一个文件,该文件从开始位置只含有20个多个字节,而以50个字节的片进行读取,这样read返回的不足值为20,此后的read将通过返回不足值0发出EOF信号。

        *从终端读文本行。如果打开文件是与终端相关联的(键盘显示器)那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。

        *读和写网络套接字,如果打开的文件对应网络套接字,那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值,对linux管道pipe调用read和write也可能出现不足值。

        如果想创建健壮的应用,必须反复调用read和write处理不足值,直到所有需要的字节都传送完毕。

5、用RIO包健壮的读写

        RIO(robust I/O,健壮的I/O包)。他它会自动为你处理上文中所述的不足值,像网络程序很容易出现不足值的应用中,RIO包提供了方便、健壮的高效IO。RIO提供了两类不同的函数:

        *无缓冲的输入输出函数,这些函数直接在内存和文件之间传送数据,没有应用级缓冲,它们对将二进制数据读写到网络和从网络读写二进制数据尤为有用。

        *带缓冲的输入函数。这些函数允许你高效的从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,带缓冲的RIO输入函数是线程安全的,它在同一个描述符上可以被交错的调用。

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

        通过调用rio_readn和rio_writen函数,应用程序可以直接在内存和文件之间传送数据。

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;      //本次读到0个字符,再次读取
        else
        return -1;      //出错,errno由read设置
    } 
    else if (nread == 0) //读取到EOF
        break;              
    nleft -= nread; //剩下的字符数减去本次读到的字符数
    bufp += nread;  //缓冲区指针向右移动
    }
    //返回实际读取的字符数
    return (n - nleft);         /* return >= 0 */
}

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)  
        nwritten = 0;    
        else
        return -1;       
    }
    nleft -= nwritten;
    bufp += nwritten;
    }
    return n;
}

        rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf,类似rio_writen函数从位置usrbuf传送n个字节到描述符fd,rio_read函数在遇到EOF时只能返回一个不足值,rio_writen函数绝不会返回不足值。上图为源码。

    5.2 RIO的带缓冲的函数

        rio_readlineb,从一个内部缓冲区复制一个文本行,当缓冲区变空时,会自动调用read重新填满缓冲区,对于既包含二进制数据的文件,提供rio_readn带缓冲区的版本,rio_readnb,它从和rio_readlineb一样的读缓冲区中传送原始字节。相关函数原型:

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;
}

static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
    int cnt;

    while (rp->rio_cnt <= 0) {  //缓冲区为空,调用read填充
    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;
}

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) 
            return -1;          /* errno set by read() */ 
    else if (nread == 0)
        break;              /* EOF */
    nleft -= nread;
    bufp += nread;
    }
    return (n - nleft);         /* return >= 0 */
}

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') {
                n++;
            break;
            }
    } else if (rc == 0) {
        if (n == 1)
        return 0; //第一次读取就到了EOF
        else
        break;    //读了一些数据后遇到EOF
    } else
        return -1;    /* Error */
    }
    *bufp = 0;
    return n-1;
}

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

        rio_readlineb函数从文件rp读处下一行文本行(包括结尾的换行符)将它复制到内存位置usrbuf,并用NULL字符来结束这个文本行,rio_readlineb函数最多读maxlen-1个字节,余下的一个字符留给结尾处的NULL字符,超过maxlen-1字节的文本行都被截断并用NULL结束。

        rio_readnb函数从文件rp最多读n个字节到内存usrbuf。对同一描述符,这些带缓冲的函数调用可以互相交叉。

6、读取文件元数据

        应用程序能够通过stat和fstat函数,检索到关于文件的信息,有时候也成为文件的元数据。

        (https://blog.csdn.net/zl6481033/article/details/86011744

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);

struct stat {
    dev_t     st_dev;     /* ID of device containing file */
    ino_t     st_ino;     /* inode number */文件的inode
    mode_t    st_mode;    /* protection */文件的权限
    nlink_t   st_nlink;   /* number of hard links */文件的硬链接个数
    uid_t     st_uid;     /* user ID of owner */文件的UID
    gid_t     st_gid;     /* group ID of owner */文件的GID(组ID)
    dev_t     st_rdev;    /* device ID (if special file) */
    off_t     st_size;    /* total size, in bytes */文件的大小
    blksize_t st_blksize; /* blocksize for file system I/O */
    blkcnt_t  st_blocks;  /* number of 512B 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 status change */文件的最后用户改变时间
           };

        stat函数以一个文件名作为输入,并填写上图stat数据结构中的各个成员。fstat函数相似,只不过是以文件描述符输入。后面会涉及到stat数据结构中的st_mode和st_size成员。st_mode成员编码了文件访问许可位。linux在sys/stat.h中定义了宏谓词来确定st_mode成员的文件类型。S_ISREG,这是一个普通文件吗。S_ISDIR,这是一个目录文件吗,S_ISSOCK,这是一个网络套接字吗。

7、读取目录内容

        应用程序可以用readdir系列函数读取目录内容。如果出错,readdir返回NULL,并设置errno。可惜的是唯一能区分错误和流结束情况的方法是检查自调用readdir以来errno是否被修改过。

#inlcude<sys/types.h>
#include<dirent.h>

//以路径名为参数,返回指向目录流的指针,流是针对条目有序列表的抽像。
DIR *opendir(const char *name);

//返回的是指向流dirp在下一个目录项的指针,如果没有更多的目录项返回NULL,每个目录项都是一个结构体
struct dirent *readdir(DIR *dirp);

//虽然有的linux版本包含了其他数据成员,但这两个对所有系统来说都是标准的。d_name是文件名,
//d_ino是文件位置
struct dirent{
    ino_t d_ino;
    char d_name[256];
}

        函数closedir关闭并释放所有资源。   

8、共享文件    

        可以用许多不同的方式共享linux文件。更具体参考(https://blog.csdn.net/zl6481033/article/details/85285697)其中文件共享章节。

9、IO重定向

    参考(https://blog.csdn.net/zl6481033/article/details/85285697),一种是在shell中进行重定向。一种是利用dup2函数。

10、标准I/O

        C语言定义了一组高级输入输出函数,称为标准I/O库。为程序员提供Unix I/O的较高级别的替换。这个库提供了打开和关闭文件的函数(fopen和fclose)读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs)、以及复杂的格式化的IO函数(scanf和printf)。

        标准的I/O库将一个打开的文件模型化为一个流,对程序员而言一个流就是一个指向FIFE类型的结构的指针。每个ANSI C 程序一开始都有三个打开的流,stdin,stdout和stderr,分别对应于标准输入、标准输出和标准错误。

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

        类型为FIFE的流是对文件描述符和流缓冲区的抽象。

11、具体使用情况

        这一章所有的IO函数。

        

        在程序中该使用这些函数中的哪一个,下面是一些指导原则:

        (1)只要有可能就使用标准I/O函数。对磁盘和终端设备I/O来说,标准I/O函数是首选方法,大部分C程序员整个职业生涯中都只使用标准I/O,除了stat函数之外。标准库中没有与其对应的函数。

        (2)不要使用scanf和rio_readlineb来读取二进制文件。这两个函数是用来专门读取文本文件的,用这些函数读取二进制文件会出现很多莫名其妙的错误。

        (3)对网络套接字的I/O使用RIO函数,用标准IO会出现一些问题,linux对网络的抽象是一种套接字的文件类型,就像所有的linux文件一样,套接字由文件描述符引用,这种情况下称为套接字描述符,应用程序进程通过读写套接字文件描述符来与运行在其他计算机的进程实现通县。

        标准I/O流,从某种意义上是全双工的,因为程序能够在同一个流上执行输入和输出,然而对流的限制和对套接字的限制有时候有冲突。

        限制一:跟在输出函数之后的输入函数。如果中间没有插入对fflush、fseek、fsetpos或rewind的调用,一个输入函数不能跟随在一个输出函数之后,fflush函数清空与流相关的缓冲区,后三个函数使用Unix I/O lseek 函数重置当前位置。

        限制二:跟在输入函数之后的输出函数。如果中间没有插入对fflush、fseek、fsetpos或rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非这个输入函数遇到一个文件结束。

        这些限制给网络应用带来一个问题,因为对套接字使用lseek函数是非法的。对流 I/O的第一个限制,能够通过采用在每个输入操作前刷新缓冲区这样的规则来满足。满足第二个限制的唯一办法就是对同一个打开套接字描述符打开两个流,一个用来读,一个用来写。但这样也会存在一些问题,必须要应用程序在两个流上调用fclose。这样的操作每一个都试图关闭同一个底层的套接字描述符,所以第二个close会失败,对于顺序的程序来说,这不是什么问题,但是在在一个线程化的程序中关闭一个已经关闭了的描述符是会导致灾难的。

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

12、小结 

        总的来说,一般情况下都是最好使用标准I/O函数,但是在网络编程中使用RIO的I/O函数。

        

        

        

猜你喜欢

转载自blog.csdn.net/zl6481033/article/details/85840512
今日推荐