《UNIX环境高级编程》第14章 高级IO

14.1 引言

本章涵盖众多概念和函数,将是后几章的基础。

14.2 非阻塞IO

10.5节中曾将系统调用分成两类:“低速”系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些文件类型的数据并不存在,该操作可能会使调用者永远阻塞;
  • 如果数据不能被相同的文件类型立即接受,写操作可能会使调用者永远阻塞;
  • 在某种条件发生之前打开某些文件类型可能会发生阻塞;
  • 对已经加上强制性记录锁的文件进行读写;
  • 某些ioctl操作;
  • 某些进程间通信函数。
    非阻塞IO使我们可以发出open、read和write这样的IO操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示这些操作如果继续执行将阻塞。
    对于一个给定的描述符,有两种为其制定非阻塞IO的方法。

    1. 如果调用open获得描述符,则可指定O_NONBLOCK标志。
    2. 对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

14.3 记录锁

当两个人同时编辑一个文件时,其后果将如何呢?在大多数UNIX系统中,该文件最后状态取决于写该文件的最后一个进程。但是对于有些应用程序,如数据库,进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。
记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。“记录”这个词是一种吴用,更适合的术语可能是“字节范围锁”(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
1.历史
早起的UNIX并不支持对部分文件加锁。
2.fcntl记录锁

#include <fcntl.h>
int fcntl(int fd,int cmd,.../*struct flock *flockptr*/);

低于记录锁,cmd是F_GETLK、F_SETLK、F_SETLKW.第3个参数是指向flock结构的指针。

struct flock {
short l_type;   //F_RDLCK/F_WRLCK,or F_UNLCK
short l_whence; //SEEK_SET,SEEK_CUR,SEEK_END
off_t l_start;  //offset in bytes,relative to l_whence
off_t l_len;    //length,in bytes;0 menas to EOF
pid_t l_pid;    //returned with F_GETLK
};

对flock结构说明如下:
- 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁)。
- 要加锁或解锁区域的起始字节偏移量(l_start和l_whence)。
- 区域的字节长度(l_len).
- 进程的ID(l_pid)持有的锁能阻塞当前进程。


总的来说记录锁实现的是对文件局部加锁的功能。

14.4 IO多路转接

如果从多个输入读取,就不能使用阻塞IO的实行处理输入,因为有可能被阻塞在其中一个输入上,而不能得到另一个IO的数据。例如telnet应用:
telnet进程读取用户输入,并通过网络送给远端主机的telnetd进程,telnetd守护进程将数据送给shell处理,并将shell返回的数据通过网络传给telnet进程,telnet进程再送到标准输出。
这样,telnet进程同时读取标准输入和来自远端telnetd进程的网络输入。因此使用阻塞IO读取其中一个都不恰当。
可以考虑使用多线程或多进程来分别处理两个IO输入,但这并不是最优的方案。
一种比较好的技术是使用IO多路转接(IO multiplexing)。为了使用这种技术,先构造一张我们感兴趣的描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行IO时,该函数才返回。
poll、pselect和select这3个函数使我们能够执行IO多路转接。在这些函数返回时,进程会被告知哪些描述符已经准备好可以进程IO了。

14.4.1 函数select和pselect

在所有POSIX兼容平台上,select函数使我们可以执行IO multiplexing ,传递给select的参数告诉内核:

  • 我们所关心的描述符;
  • 对于每个描述符我们所关心的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符写,是否关心一个给定描述符的异常条件);
  • 愿意等待多长时间(可以用于等待、等待一个固定的时间或者根本不等待)。
    从select返回时,内核告诉我们:

  • 已准备好的描述符的总数量;

  • 对于读、写或异常这3个条件中的每一个,哪些描述符已经准备好。
    使用这种返回信息,就可以调用相应的IO函数(一般是read或write),并且确知该函数不会阻塞。
#include <sys/select.h>
int select(int maxfdpl,fd_set *restrict readfds,fd_set *restrict write,fd_set *restrict exceptfds,struct timeval *restrict tvptr );

参数tvptr,它指定愿意等待的时间长度,单位为秒和微秒。

  • tvptr ==NULL,永远等待,若捕捉到一个信号则中断此无限等待。
  • tvptr ->tv_sec==0&&tvptr ->tv_usec==0,根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞select函数的方法。
  • tvptr ->tv_sec!=0&&tvptr ->tv_usec!=0,等待指定的秒数和微秒。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。
    中间3个参数readfds、writefds、exceptfds是指向描述符集的指针。这3个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个fd_set数据类型中。
#include <sys/select.h>
int FD_ISSET(int fd,fd_set *fdset); //测试是否打开
void FD_CLR(int fd,fd_set *fdset);  //清除一位
void FD_SET(int fd,fd_set *fdset);  //设置一位
void FD_ZERO(fd_set *fdset);        //将整个描述符集置0

第一个参数maxfdpl的意思是“最大文件描述符编号值加1”。

select函数有3个返回值:
1. 返回值-1表示出错。例如,在指定的描述符一个都没有准备好时捕捉到了一个信号。在此种情况下,一个描述符集都不修改。
2. 返回值0,表示没有描述符准备好。若指定的描述符一个都没有准备好,指定的时间就过完了,那么就会发生这种情况。此时所有描述符集都会置0。
3. 一个正返回值说明了已经准备好的描述符数。该值是3个描述符集中已经准备好的描述符数之和,所有如果同一描述符已经准备好读和写,那么在返回值中会对其计数两次。在这种情况下,3个描述符集中仍旧打开的位对应于已经准备好的描述符。


POSIX也定义了一个select的变体,称为pselect。

14.4.2 函数poll

poll函数类似于select,但是程序员接口有所不同。poll函数可用于任何类似文件描述符。

#include <poll.h>
int poll(struct pollfd fdarray[].nfds_t nfds,int timeout);

与select函数不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件

struct pollfd{
int fd;
short events;
short revents;
};

这里写图片描述
poll函数中:
events成员设置为上面所示的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。
revents成员由内核设置,用于说明每个描述符发生了哪些事件。

14.5 异步IO

使用上一节说明的select和poll可以实现异步形式的通知。关于描述符的状态,系统并不主动告诉我们任何信息,我们需要进行查询(调用select或poll)。如在第10章中所述,信号机构提供了一种以异步形式通知某种事件已经发生的方法。
在我们了解使用异步IO的不同方法之前,需要先讨论一下成本。在用异步IO的时候,要通过选择来灵活处理多个并发操作,这会使应用程序的设计复杂化。更简单的做法可能是使用多线程,使用同步模型来编写程序,并让这些线程以异步的方法运行。

14.5.1 System V异步IO

在System V中,异步IO是STREAMS系统的一部分,它只对STREAMS设备和STREAMS管道起作用。System V的异步IO信号是SIGPOLL.
为了对一个STREAMS设备启动异步IO,需要调用ioctl,将它的第二个参数(request)设置成I_SETSIG.
处理调用ioctl指定产生SIGPOLL信号的条件以外,还应为信号建立信号处理程序。对于SIGPOLL的默认动作是终止该进程,所以应当在调用ioctl之前建立信号处理程序。

14.5.2 BSD异步IO

在BSD派生的系统中,异步IO是信号SIGIO和SIGURG的组合。SIGIO是通用异步IO信号,SIGURG则只用来通知进程网络连接上的带外数据已经到达。

14.5.3 POSIX异步IO

POSIX异步IO接口对不同类型的文件进行异步IO提供了一套一致的方法。
这些异步IO接口使用AIO控制块来描述IO操作。aiocb结构定义了AIO控制块。该结构至少包含以下字段:

struct aiocb{
int         aio_fildes; //被打开用来读或写的文件描述符
off_t       aio_offset; //读或写的操作从aio_offset指定的偏移量开始
volatile    void *aio_buf;  //对于读操作,数据复制到该缓冲区
size_t      aio_nbytes; //包含了要读或写的字节数。
int         aio_reqprio;    //异步IO请求提示顺序(系统对此顺序只有有限的控制力,并不一定按照此顺序)
struct      sigevent aio_sigvent;   //此字段控制在IO事件完成后,如何通知应用程序。
int     aio_lio_opcode;     //仅用于基于列表的异步IO
};

aio_sigevent字段控制在IO事件完成后,如何通知应用程序,这个字段通过sigevent结构来描述。

struct sigevent{
int sigev_notify;
int sigev_signo;
unio sigval sigev_value;
void(*sigev_notify_function)(union sigval);
pthread_attr_t  *sigev_notify_attributrs;
};

sigev_notify 字段控制通知的类型。取值可能是以下3个中的一个:
- SIGEV_NONE 异步IO请求完成后,不通知进程。
- SIGEV_SIGNAL 异步IO请求完成后,产生由sigev_signo字段指定的信号
- SIGEV_THREAD 当异步IO请求完成时,由sigev_notify_function字段指定的函数被调用sigev_value字段被传入作为它唯一参数。除非sigev_notify_attributes字段被设定为pthread属性结构的地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行

在进行异步IO之前需要先初始化AIO控制块,调用aio_read函数来进一步进行异步读操作,或调用aio_write函数来进行异步写操作:

#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);

当这些函数返回时,异步io请求便已经被操作系统放入等待处理的队列中了。


要想强制所有等待中的异步操作不等待而写入持久化的存储中(直接flush,不缓冲),可以设立一个AIO控制块,并调用aio_fsync函数。

#include <aio.h>
int aio_fsync(int op,struct aiocb *aiocb);

AIO控制块中的aio_fildes字段指定了其异步写操作被同步的文件。如果op参数设定为O_DSYNC,那么操作执行起来就像调用了fdatasync一样,否砸,如果op参数设定为O_SYNC那么操作执行起来就会像调用了fsync一样。


为了获知一个异步读、写或者同步操作的完成状态,需要调用aio_error函数。

#include <aio.h>
int aio_error(const struct aiocb *aiocb);

返回值为下面4种情况中的一种。
0 异步操作成功完成。需要调用aio_return函数获取操作返回值。
-1 对aio_error调用失败,这时候errno会告诉我们为什么。
EINPROGRESS 异步读、写或同步操作仍在等待。
其他情况 其他任何返回值是相关的异步操作失败返回的错误码。


为了获得一个异步读、写或者同步操作的完成状态,需要调用aio_return函数。

#include <aio.h>
int aio_return(const struct aiocb *aiocb);

注意,直到异步操作完成之前,都需要小心不要调用aio_return函数。操作完成之前的结果是为定义的。还需要小心对每个异步操作只调用一次aio_return,一旦调用了该函数,操作系统就可以释放掉包含io操作返回值的记录。


执行IO操作时,如果还有其他事物需要处理而不想被IO操作阻塞,就可以使用异步IO。然而,如果在完成了所有事物时,还有异步操作未完成时,可以调用aio_suspend函数来阻塞进程,直到操作完成。

#include <aio.h>
int aio_suspend(const struct aiocb *const list[],int nent,const struct timespec *timeout);
//const struct aiocb *const list[],第一个const表示指针指向的值不可变,第二个const指指针本身不可变;
  • list参数是一个指向AIO控制块数组的指针,
  • nent参数表明了数组中的条目数。
  • timeout参数表明了阻塞时间限制。
    ai_suspend可能会返回三种情况中的一种:

  • 如果我们被一个信号中断,它将会返回-1,并将errno设置为EINTR.

  • 如果没有任何IO操作完成的情况下,阻塞时间超过了函数中可选的timeout参数指定的时间限制,那么返回-1,并将errno设置为EAGAIN。(若不想设置任何时间限制的话,可以给timeout空指针)
  • 如果有任何IO操作完成,ai_suspend将返回0.
    如果所有的异步IO操作都已完成,那么aio_suspend将在不阻塞的情况下直接返回。

当还有我们不想再完成的等待中的异步IO操作时,可以尝试使用aio_cancel函数来取消它们:

#include <aio.h>
int aio_suspend(int fd,const struct aiocb *aiocb);

fd参数指定了那个未完成的异步IO操作的文件描述符。如果aiocb参数设置为NULL,系统将会尝试取消所有该文件上未完成的异步IO操作。之所以是“尝试”,是因为无法保证系统能够取消正在进程中的任何操作。
aio_calcel函数可能会返回一下4个值中的一个:

  • AIO_ALLDOWN 所有操作在尝试取消它们之前已经完成。
  • AIO_CANVELED 所有要求的操作已被取消。
  • AIO_NOTCANCELED 至少有一个要求的操作没有被取消。
  • -1 对aio_cancel的调用失败,错误码存储在errno中。
    如果异步IO操作被成功取消,对相应的AIO控制块调用aio_error函数将会返回错误ECANCELED。如果操作不能被取消,那么相应的AIO控制块不会因为对aio_cancel的调用而被修改。

还有一个函数也被包含在异步IO接口中,尽管它既能以同步的方式来使用,又能以异步的方式来使用,这个函数就是lio_listio。该函数提交一系列由一个AIO控制块列表描述的IO请求。

#include <aio.h>
int lio_listio(int mode,struct aiocb *restrict const list[restrict],int nent,struct sigevent *restrict sigev);
//方括号中的restrict是什么意思???
  • mode参数决定了IO是否真的是异步的。如果改参数为LIO_WAIT,函数将在所有由列表指定的IO操作完成后返回。这种情况下,sigev参数将被忽略。如果设定为LIO_NOWAIT,函数将在IO请求入队后立即返回。进程将在所有IO操作完成后,安装sigev参数指定的,被异步地通知。如果不想被通知可以把sigev设置为NULL。
  • list参数指向AIO控制块列表,该列表指定了要运行的IO操作。列表中可以包含NULL,这些条目会被忽略。
  • nent参数指定来数组中的元素个数。

每个AIO控制块中,aio_lio_opcode地段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是被忽略的空操作(LIO_NOP)。
引入POSIX异步操作IO接口的初衷是为实时应用提供一种方法,避免在执行IO操作时阻塞进程。

14.6 函数readv和writev

readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec *iov,int iovcnt);
ssize_t writev(int fd,const struct iovec *iov,int iovcnt);

这两个函数的第二个参数是指向iovec结构数组的一个指针;

struct iovec {
void *iov_base; //buf起始地址
size_t iov_len; //buf大小
};

iov数组中的元素由iovcnt指定,其最大值受限于IOV_MAX.

14.7 函数readn和writen

管道、FIFO以及某些设备(特别是终端和网络)有下列两种性质:

  1. 一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样。这不是一个错误,应当继续读该设备。
  2. 一次write操作的返回值也可能少于指定输出的字节数。这可能是由某个因素造成的,例如,内核输出缓冲区满。这也不是错误,应当继续写余下的数据。
    通常在读、写一个管道、网络设备或终端时,需要考虑这些特性。readn和writen函数的功能分别是读、写指定的N字节数据,并处理返回值可能小于要求值的情况。这两个函数只是按需多次调用read和write直至读、写了N字节数据。
#include <sys/uio.h>
ssize_t readn(int fd,void *buf,size_t nbytes);
ssize_t writen(int fd,void *buf,size_t nbytes);

14.8 存储映射IO

存储映射IO(memory-mapped IO)能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样就可以在不使用read和write的情况下执行IO。

为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。

#include <sys/mman.h>
void *mmap(void *addr,size_t len,int port,int flag,int fd,off_t off);
  • addr参数用于指定映射存储区的起始地址。通常设置为0,这表示由系统选择该映射区的起始地址。此函数的返回值是映射区的起始地址。
  • len参数是映射的字节数。
  • port 参数指定了映射存储区的保护要求。可以是可读(PORT_READ)、可写(PORT_WRITE)、可执行(PORT_EXEC)、不可访问(PORT_NONE).
  • flag参数影响映射存储区的多种属性。
  • fd参数是指定要被映射的文件描述符。在文件映射到地址空间之前,必须先打开该文件。
  • off是要映射字节在文件中的起始偏移量。
  • -

子进程通过fork继承存储映射区(因为子进程复制父进程地址空间,而存储区映射是该地址空间的一部分)。新程序则不能通过exec继承存储映射区。


mprotect函数可以更改一个现有的映射权限:

#include <sys/mman.h>
void mprotect(void *addr,size_t len,int prot);

prot的合法值与mmap中prot参数一样。注意,地址参数addr的值必须是系统页长的整数倍。


如果共享映射中的页已经修改,那么可以调用msync将该页冲洗到被映射的文件中。msync函数类似于fsync,但用于存储映射区。

#include <sys/mman.h>
void msync(void *addr,size_t len,int flags);

当进程终止时,会自动解除映射区的映射,或者直接调用munmap函数也可以解除映射区。关闭映射存储区使用的文件描述符并不解除映射区。

#include <sys/mman.h>
int munmap(void *addr,size_t len);

munmap并不影响被映射的对象,也就是说,调用munmap并不会使映射区的内容写到磁盘文件上。

14.9 小结

本章介绍了很多高级IO功能。

  • 非阻塞IO;
  • 记录锁;
  • IO多路转接;
  • readv和writev函数。
  • 存储映射IO(mmap)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章