APUE:I/O概要

1.文件I/O

文件描述符是内核对于进程中所打开文件所提供的引用.

其中Unix系统shell将0,1,2分别对应标准输入,标准输出和标准错误(STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO).需要注意的是文件描述符对于每个进程有数量限制.

文件I/O的操作是紧紧围绕文件描述符展开的.

1) 基本函数

open和openat.返回的都是fd.其中第二个参数都有oflag,这是一个整型数.最常用的也就是0,1,2(分别对应O_RDONLY,O_WRONLY,O_RDWR).除此之外,还有一些比较常见的:O_APPEND,O_CREAT,O_TRUNC,O_NONBLOCK等.其返回的文件描述符都是最小的未使用的描述符数值.

对于以上两者的区别在于openat多了一个fd参数.这个fd通常对应一个目录,也就是说在openat中path作为相对路径会在fd对应的目录下有效.如下:

dir_fd = open(dir_path, O_RDONLY);
if (dir_fd < 0) {  
		perror("open");  
    exit(EXIT_FAILURE);  
 }   
 flags = O_CREAT | O_TRUNC | O_RDWR;  
 mode = 0640;  //-rw-r-----
 fd = openat(dir_fd, relative_path, flags, mode);  

关于create,最初之所以设计这个调用,只因为open的第二个参数所支持的模式很有限,只限于0,1,2.在后来O_CREATE,O_TRUNC出现后,open可以完全替代create的使用.需要注意的是,它是只写的.如果想要先写再读的话,只能先create然后close,最后read

关于close.没有太多值得说的地方.会释放进程加在该文件上的记录锁,当一个进程终止时,会将其中打开的所以fd对应文件关闭(记录锁)释放.

lseek返回值off_t,描述的是字节,这通常是一个非负数,并且适应于跨平台的需要.第三个参数whence指定了offset的起始方式,有SEEK_SET,SEEK_CUR,SEEK_END,后两者都是可正可负的.不是所有文件描述符都可以被设置偏移量,像pipe,socket,FIFO等会返回-1.lseek没有实际的I/O操作,只是修改了内核中的变量.

read,三个参数文件描述符,缓冲区指针,读取字节数.其重点在于掌握其返回值,0表示到头了.正常情况下,应该与第三个参数相等.但是对于不同类型的文件,其返回值特点不同(比如说从网络上读取,返回值可能小于期望值).

普通文件的话,如果以100字节为读取的步数,最终只剩50,那么就返回50,再次调用时才返回0.从网络设备中读,可能返回值小于第三个参数.其返回值ssize_t是有符号数,这样就可以返回-1了.

write返回值通常与第三个参数相同,如果出错一般是磁盘写满了,或者超过进程对文件长度的限制.对于写入的地址,如果文件指定了APPEND,就直接追加.否则从当前offset写.

为什么很多函数的返回值使用int或者ssize_t?

既可以表示正常的返回(非负数),以负数(-1)表示出错的情况.

关于头文件,open,creat等是<fcntl.h>,其他的是<unistd.h>.

2) 文件共享

这里主要考虑的问题在于Unix系统上不同进程共享打开文件的问题.

首先介绍的是,内核维护的打开文件的数据结构.

image-20220818174714835

linux系统中没有v节点,使用通用inode.

对于共享相同文件的进程来说,虽然其描述符不同,但是最终指向相同的inode.如图下所示.

image-20220818175302163

文件表项在每个进程中不同.其中记录了状态和偏移等信息.简而言之,对于不同进程共享打开文件的状况,虽然在进程内维护的文件表项不一样,但是表项中指向的i节点是同一个.而对于文件状态和当前文件的偏移量都是属于表项的内容,而不是inode中的内容,因此不同的进程可以不同.

在多进程操作文件时,容易造成一些不一致的状态,比如说,如果不使用O_APPEND进行文件的追加,使用lseek和write的话,如果第一个进程调用完lseek被切换到另一个进程,另一个进程将lseek和write都做完的话,然后又切换到第一个进程就会出现问题.避免这种问题的方式就是将lseek和write做成一个原子操作,如果使用O_APPEND的就可以解决这个问题.

类似的问题还有,先用open检查文件存在,如果没有再creat的情景,如果中间穿插另一个进程创建文件,并且写入的情况,就会导致被擦除.所以使用带有O_CREAT的open是更好的解决方案.

此外pwrite,pread是类似的接口,简而言之实现了lseek和read(或者write)的原子性,不过不会改变内核中的文件偏移.

3) dup,fcntl,sync,fsync,ioctl等函数

dup的作用在于复制一个现有的文件描述符.在内核上,体现为使得不同的描述符指向同一个文件表项,因此对于文件的状态和偏移量也是共享的.使用fcntl也可以复制描述符,dup(fd)等同于fcntl(fd,F_DUPFD,0),dup2等同于close(fd2);fcntl(fd,F_DUPFD,fd2);.后者也不算完全等同,因为dup2是原子操作.

对于sync,fsync等函数,其设计的前提是基于,unix延迟写的特性.sync只用于将所有修改过的块缓冲区排入写队列,并不是等待写入磁盘结束.

fcntl函数则用于一些对于改变已经打开的文件的属性.其中第二个参数cmd有F_DUPFD,F_GETFD,F_SETFD,F_GETFL等.

2.标准I/O

正如文件I/O围绕着fd一样,标准I/O是围绕着stream进行的.比如说同样是对于文件的读写,标准IO则是对FILE *(文件流对象指针)进行操作.

fopen是打开一个文件流的常用方式,返回一个FILE指针.FILE中包含了文件描述符,缓冲区指针,缓冲区长度等.

1) 流Buffer

标准I/O的缓冲有三种:全缓冲,行缓冲,不带缓冲.

全缓冲等到填满了buffer后进行实际的I/O操作,行缓冲只要遇到换行符就执行实际的I/O操作.不带缓冲的话,比如说fputs,stderr.

在很多系统中,默认为:

  • stderr不带缓冲.
  • 指向终端设备,则是行缓冲,否则全缓冲.

除此之外,还有相应的API用来设置buffer.其参数有一个FILE*和buffer.setbuf,setvbuf后者还有mode和size两个参数,mode指定缓冲的三种方式.而fflush则是用于将buffer内容写入.

image-20220819092518549

2) 常规的流操作

其中有fopen,freopen,fdopen用来打开标准I/O流.fdopen常用于打开一些不能够通过流直接打开(fopen)的文件类型,比如说socket, pipe等返回的描述符.对于其中的参数type,主要有r,w,a,r+,w+等.

fclose在关闭时会对流中的buffer进行flush.如果这个buffer还是系统分配则会被释放.当进程正常结束,打开的标准I O会被冲洗并关闭.

3.高级I/O

本章的主要内容有非阻塞I/O,记录锁,I/O复用,异步I/O,readv和writev等.

1) I/O模型

其中主要的概念有非阻塞I/O,I/O复用,异步I/O等.

image-20220818185535727

有关于非阻塞demo,将一个文件描述符(STDOUT)设置为非阻塞的.将一个缓冲区中的数据写入如下所示:

ntowrite=read(STDIN_FILENO,buf,sizeof(buf));
fprintf(stderr,"read %d bytes\n ",ntowrite);
set_fl(STDOUT_FILENO,O_NONBLOCK);  //设置为非阻塞
ptr=buf;
while(ntowrite>0) //这是一个非阻塞
{
    errno=0;
    nwrite=write(STDOUT_FILENO,ptr,ntowrite);
    fprintf(stderr,"nwrite=%d,errno=%d\n",nwrite,errno);

    if(nwrite>0)
   {
        ptr+=nwrite;
        ntowrite-=nwrite;
    }
}

简而言之,这是一个非阻塞I/O+轮询的模式,在这个demo中,while循环会进行9000次write操作,其中只有500次真正输出,其他时候属于缓冲区没有数据可写的情况.在这里,对于非阻塞I/O进行写操作造成了许多次无用的写操作,我们希望只有可写的时候才会调用write,而不是像这样进行轮询.

2) 记录锁

3) I/O 复用

当我们需要对多个描述符进行读写的时候,这个时候在一个循环上阻塞在某个描述符上,就会耽误对其他描述符的读写.这也就是I/O复用的最常见的用途.

如果对每个需要读写的描述符都分配一个线程进行处理一个描述符呢?这样一是开销过大,而且对于进程的回收时机很难确定,需要额外的引入机制来终止进程.如果使用多线程的话,仍然不是好的方案,额外的同步机制增加了复杂度.

如果都设置为非阻塞read并且采用for循环轮询,但是浪费CPU时间.

如果采用异步I/O的方式.除了可移植性的问题,另外一种问题在于异步信号难以分辨出是哪一个描述符的.如果采用不同的信号,又该如何应对大量描述符的情况呢?

因此,I/O复用是一种比较好的解决方式,基本思想在于,构造描述符及其对应的事件表,当其中有描述符的时间准备好,I/O复用函数返回,否则就阻塞.

其中主要有:select,pselect,epoll等.

关于select,其中的参数有:描述符,触发条件,等待时间.返回时:就绪描述符数量,3个条件中就绪的描述符.

int select(int, fd_set * __restrict, fd_set * __restrict,
    fd_set * __restrict, struct timeval * __restrict) 

对于fd_set的设置需要借助特定的API,比如说,FD_ISSET,FD_CLR,FD_SET,FD_ZERO等.

关于poll,需要构造的并不是对每种条件的描述符集合,而是构造pollfd的数组.其中这个结构体的内容为:fd,期望事件,返回事件.第二个参数指定数组的数量.关于其中timeout,-1无限等待,0则会退化成轮询.

extern int poll(struct pollfd *, nfds_t, int) __DARWIN_ALIAS_C(poll);

其中描述这些时间的标志非常重要.

image-20220818231644905

将select,epoll,poll等进行对比会怎样??

4) readv,writev和readn,writen

ready,writev和一般读写的区别在于,其中的buffer不是连续的,而是多个分散的buffer.其中读写的顺序是在数组中逐个顺序地读写的.

对于readn,writen主要应对管道,FIFO,网络等场景,其读写返回的数据可能少于要求的数据量.这种情况下,应该继续进行读写处理.

int readn(int fd, void *vptr, size_t n){
    size_t          nleft = n;          //readn函数还需要读的字节数
    ssize_t        nread = 0;          //read函数读到的字节数
    unsigned char  *ptr = (char *)vptr; //指向缓冲区的指针
    while (nleft > 0){
        nread = read(fd, ptr, nleft);
        if (-1 == nread){
            if (EINTR == errno)
                nread = 0;
            else
                return -1;
        }
        else if (0 == nread){
            break;
        }
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}
int writen(int fd, const void *vptr, size_t n){
    size_t          nleft = n;  //writen函数还需要写的字节数
    ssize_t        nwrite = 0; //write函数本次向fd写的字节数
    const char*    ptr = vptr; //指向缓冲区的指针

    while (nleft > 0){
        if ((nwrite = write(fd, ptr, nleft)) <= 0){
            if (nwrite < 0 && EINTR == errno)
                nwrite = 0;
            else
                return -1;
        }
        nleft -= nwrite;
        ptr += nwrite;
    }
    return n;
}

5) mmap

mmap的作用在于将一个buffer与磁盘文件相映射.读写缓冲区也就成了对文件的读写.

void *  mmap(void *, size_t, int, int, int, off_t) __DARWIN_ALIAS(mmap);//如果将buffer的地址设置为0,表示由系统分配的,其中prot指定了保护要求,PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE等.

其中第四个参数指定了影响buffer的多种属性.