CSAPP-系统级I/O

编程/技术 2019-05-14 @ 08:43:40 浏览数: 71 净访问: 68 By: skyrover

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议


希望变成一个很牛的程序员,对计算机系统以及它们对程序的影响有很成熟的理解。

输入操作是从I/O设备拷贝数据到主存,而输出操作是从主存拷贝数据到I/O设备。

10.1 Unix I/O

一个Unix文件就是一个m个字节的序列:B0,B1,...,Bk,...,Bm-1,所有I/O设备,如网络,磁盘和终端,都被模型化为文件,所有输入和输出都被当做对相应文件的读和写来执行。

  • 打开文件,描述符
  • 改变当前文件位置
  • 读写文件
  • 关闭文件,内核释放文件打开时候创建的数据结构,并将这个描述符恢复到可用的描述符池中,无论一个进程为何种原因终止时,内核都会关闭所有打开的文件并且释放它们的存储器资源

10.2 打开和关闭文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(char *fileame, int flags, mode_t mode);

返回一个文件描述符数字,总是当前进程中没有打开的最小描述符。flags表示对应的打开动作:

  • O_RDONLY: 只读
  • O_WRONLY: 只写
  • O_RDWR: 可读可写
  • O_CREAT: 创建
  • O_TRUNC: 已存在,就截断它
  • O_APPEND: 文件位置在结尾

mode参数制定了新文件的访问权限位,每一个进程有一个umask,利用umask命令可以指定哪些权限将在新文件的默认权限中被删除,即最终权限为mode & ~umask

关闭一个文件:

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

10.3 读和写文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);

read返回-1表示错误,0表示读到EOF,否则返回的是实际传送的字节数量。有时候,read和write传送字节比要求的少,被称为不足值。

  • 读时遇到EOF
  • 从终端读文本行
  • 读和写网络套接字:因为内部缓冲约束和网络延迟可能会引起read和write返回不足值,所以在网络应用中,需要反复调用read和write处理不足值,直到所有需要的字节都传送完毕

10.4 用RIO包健壮地读写

Robust I/O,会处理不足值。

  • 无缓冲的输入输出函数:
  • 带缓冲的输入函数

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

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){ //EOF
            break;
        }
        // nread为实际读的数据,处理了不足值
        nleft -= nread;
        bufp += nread;
    }
    return (n-nleft);
}

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

10.4.2 RIO的带缓冲的输入函数

一个文本行就是一个由换行符结尾的ASCII码字符序列,Unix系统中\n与ASCII码换行符LF相同,数字值位0x0a

对于计算文本文件中文本行数量的程序来说,通过read函数一次一个字节来读,肯定是低效的,因为每次调用read都要陷入内核。所以更好的方法是通过带有内部缓冲区的函数里进行读取,从一个内部读缓冲区拷贝一个文本行,等缓冲区变空,则自动调用read重新填满缓冲区。

#include "csapp.h"
// 将描述符fd与地址rp处的一个类型为rio_t的读缓冲区联系起来
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);

下面是一个调用的例子

#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)
    }
}

#define RIO_BUFSIZE 8192
typedef struct{
    int rio_fd;
    int rio_cnt; // 未读字节
    char *rio_bufptr; // 指向buf下一个未读字节
    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_read函数,会通过缓冲区进行读取

static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n){
    int cnt;
    // 在内部缓冲区为空的时候重新填满缓冲区
    while (rp->rio_cnt <= 0){
        rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));
        if (rp->rio_cnt < 0){
            if (errno != EINTR){
                return -1;
            }
        }else if (rp->rio_cnt == 0){
            return 0;
        }else{
            rp->rio_bufptr = rp->rio_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_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;
            }else{
                break;
            }
        } else {
            return -1;
        }
    }
    // 用空(零)字符来结束这个文本行
    *bufp = 0;
    return n;
}

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){
                nread = 0;
            }else{
                return -1;
            }
        }else if (nread == 0){
            break;
        }
        nleft -= nread;
        bufp += nread;
    }
    return (n - nleft);
}

10.5 读取文件元数据

通过调用stat和fstat函数,检索到关于文件的信息(文件元数据)

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

int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);

10.6 共享文件

内核用三个相关数据结构表示打开的文件:

  • 描述符表。每个进程有独立的描述符表,每一表项对应一个描述符,然后指向文件表中的一个表项。
  • 文件表。表示打开的文件集合,所有进程共享。包括当前文件位置,引用计数,指向v-node表中对应表项的指针。直到引用计数为0,才删除这个文件表项。
  • v-node表。所有进程共享,包含stat的大部分信息,st_mode, st_size

关于共享文件的一些补充:

  • 不同进程打开同一个文件

有不同的描述符表,而且有各自对应的文件表表项,因为文件表象记录的数据是不能在多个进程共享的。但是对应同一个v-node表项。

  • dup函数复制一个文件描述符

使用dup函数复制一个文件描述符:dup函数是用来复制一个文件描述符的。复制得到的文件描述符和原描述符共享文件偏移量和一些状态。所以dup的作用仅仅是复制一个文件描述符表项,而不会复制一个文件表表项。平时使用>进行重定向就是通过dup

  • 同一个进程多次打开同一个文件

每打开一次同一个文件,内核就在文件表中增加一个表项,因为每次open文件的读写权限什么可能不同,所以这些信息是存在文件表表项的。但是v-node表表项是一致的。

  • 父进程使用fork创建子进程

子进程会复制父进程的整个文件描述符表,所以都会引用同一个文件表项,所以在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。

10.7 I/O重定向

#include <unistd.h>
int dup2(int oldfd, int newfd);

dup2函数拷贝描述符表表项oldfd到描述符表表项newfd。如下图,复制后本来文件描述符表表项1指向的是文件A,复制后,描述符1也指向了文件表项B,所以文件A被关闭,它的文件表和v-node表表项就会被删除。

10.8 标准I/O

ANSI C定义了一组高级输入输出函数,为标准I/O库,fopen, fclose, fread, fwrite, fgets, fputs, scanf, printf

该库将一个打开的文件模型化为一个流,一个流就是一个指向FILE类型的结构的指针。程序开始时都有三个打开的流stdin, stdout, stderr

类型为FILE的流是对文件描述符和流缓冲区的抽象,流缓冲区的目的和RIO读缓冲区一样。

10.9 综合:我该使用哪些IO函数

在网络输入输出上使用标准IO,会出现问题。Unix对网络的抽象是一种套接字的文件类型,也是用文件描述符来引用的,应用进程通过读写套接字描述符来与运行在其他计算机上的进程通信。

标准IO流,程序能够在同一个流上执行输入和输出,但是有两个限制

  • 跟在输出函数之后的输入函数,必须要清空与流相关的缓冲区
  • 跟在输入函数之后的输出函数,如果没有fseek,fsetpos和rewind是不可以的。

对于套接字,第一个限制可以通过刷新缓冲区。第二个只能对同一个打开的套接字打开两个流,一个输入,一个输出。但是必须都进行fclose,这样多线程程序就会有问题。

所以不要使用标准IO函数来进行网络套接字的输入和输出。


点赞走一波😏


评论

提交评论