本章涵盖众多概念和函数,将是后几章的基础。
10.5节中曾将系统调用分成两类:“低速”系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:
某些进程间通信函数。
非阻塞IO使我们可以发出open、read和write这样的IO操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示这些操作如果继续执行将阻塞。
对于一个给定的描述符,有两种为其制定非阻塞IO的方法。
当两个人同时编辑一个文件时,其后果将如何呢?在大多数UNIX系统中,该文件最后状态取决于写该文件的最后一个进程。但是对于有些应用程序,如数据库,进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。
记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。“记录”这个词是一种吴用,更适合的术语可能是“字节范围锁”(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
1.历史
早起的UNIX并不支持对部分文件加锁。
2.fcntl记录锁
#include
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)持有的锁能阻塞当前进程。
总的来说记录锁实现的是对文件局部加锁的功能。
如果从多个输入读取,就不能使用阻塞IO的实行处理输入,因为有可能被阻塞在其中一个输入上,而不能得到另一个IO的数据。例如telnet应用:
telnet进程读取用户输入,并通过网络送给远端主机的telnetd进程,telnetd守护进程将数据送给shell处理,并将shell返回的数据通过网络传给telnet进程,telnet进程再送到标准输出。
这样,telnet进程同时读取标准输入和来自远端telnetd进程的网络输入。因此使用阻塞IO读取其中一个都不恰当。
可以考虑使用多线程或多进程来分别处理两个IO输入,但这并不是最优的方案。
一种比较好的技术是使用IO多路转接(IO multiplexing)。为了使用这种技术,先构造一张我们感兴趣的描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行IO时,该函数才返回。
poll、pselect和select这3个函数使我们能够执行IO多路转接。在这些函数返回时,进程会被告知哪些描述符已经准备好可以进程IO了。
在所有POSIX兼容平台上,select函数使我们可以执行IO multiplexing ,传递给select的参数告诉内核:
愿意等待多长时间(可以用于等待、等待一个固定的时间或者根本不等待)。
从select返回时,内核告诉我们:
已准备好的描述符的总数量;
#include
int select(int maxfdpl,fd_set *restrict readfds,fd_set *restrict write,fd_set *restrict exceptfds,struct timeval *restrict tvptr );
参数tvptr,它指定愿意等待的时间长度,单位为秒和微秒。
#include
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。
poll函数类似于select,但是程序员接口有所不同。poll函数可用于任何类似文件描述符。
#include
int poll(struct pollfd fdarray[].nfds_t nfds,int timeout);
与select函数不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。
struct pollfd{
int fd;
short events;
short revents;
};
poll函数中:
events成员设置为上面所示的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。
revents成员由内核设置,用于说明每个描述符发生了哪些事件。
使用上一节说明的select和poll可以实现异步形式的通知。关于描述符的状态,系统并不主动告诉我们任何信息,我们需要进行查询(调用select或poll)。如在第10章中所述,信号机构提供了一种以异步形式通知某种事件已经发生的方法。
在我们了解使用异步IO的不同方法之前,需要先讨论一下成本。在用异步IO的时候,要通过选择来灵活处理多个并发操作,这会使应用程序的设计复杂化。更简单的做法可能是使用多线程,使用同步模型来编写程序,并让这些线程以异步的方法运行。
在System V中,异步IO是STREAMS系统的一部分,它只对STREAMS设备和STREAMS管道起作用。System V的异步IO信号是SIGPOLL.
为了对一个STREAMS设备启动异步IO,需要调用ioctl,将它的第二个参数(request)设置成I_SETSIG.
处理调用ioctl指定产生SIGPOLL信号的条件以外,还应为信号建立信号处理程序。对于SIGPOLL的默认动作是终止该进程,所以应当在调用ioctl之前建立信号处理程序。
在BSD派生的系统中,异步IO是信号SIGIO和SIGURG的组合。SIGIO是通用异步IO信号,SIGURG则只用来通知进程网络连接上的带外数据已经到达。
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
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
当这些函数返回时,异步io请求便已经被操作系统放入等待处理的队列中了。
要想强制所有等待中的异步操作不等待而写入持久化的存储中(直接flush,不缓冲),可以设立一个AIO控制块,并调用aio_fsync函数。
#include
int aio_fsync(int op,struct aiocb *aiocb);
AIO控制块中的aio_fildes字段指定了其异步写操作被同步的文件。如果op参数设定为O_DSYNC,那么操作执行起来就像调用了fdatasync一样,否砸,如果op参数设定为O_SYNC那么操作执行起来就会像调用了fsync一样。
为了获知一个异步读、写或者同步操作的完成状态,需要调用aio_error函数。
#include
int aio_error(const struct aiocb *aiocb);
返回值为下面4种情况中的一种。
0 异步操作成功完成。需要调用aio_return函数获取操作返回值。
-1 对aio_error调用失败,这时候errno会告诉我们为什么。
EINPROGRESS 异步读、写或同步操作仍在等待。
其他情况 其他任何返回值是相关的异步操作失败返回的错误码。
为了获得一个异步读、写或者同步操作的完成状态,需要调用aio_return函数。
#include
int aio_return(const struct aiocb *aiocb);
注意,直到异步操作完成之前,都需要小心不要调用aio_return函数。操作完成之前的结果是为定义的。还需要小心对每个异步操作只调用一次aio_return,一旦调用了该函数,操作系统就可以释放掉包含io操作返回值的记录。
执行IO操作时,如果还有其他事物需要处理而不想被IO操作阻塞,就可以使用异步IO。然而,如果在完成了所有事物时,还有异步操作未完成时,可以调用aio_suspend函数来阻塞进程,直到操作完成。
#include
int aio_suspend(const struct aiocb *const list[],int nent,const struct timespec *timeout);
//const struct aiocb *const list[],第一个const表示指针指向的值不可变,第二个const指指针本身不可变;
timeout参数表明了阻塞时间限制。
ai_suspend可能会返回三种情况中的一种:
如果我们被一个信号中断,它将会返回-1,并将errno设置为EINTR.
当还有我们不想再完成的等待中的异步IO操作时,可以尝试使用aio_cancel函数来取消它们:
#include
int aio_suspend(int fd,const struct aiocb *aiocb);
fd参数指定了那个未完成的异步IO操作的文件描述符。如果aiocb参数设置为NULL,系统将会尝试取消所有该文件上未完成的异步IO操作。之所以是“尝试”,是因为无法保证系统能够取消正在进程中的任何操作。
aio_calcel函数可能会返回一下4个值中的一个:
还有一个函数也被包含在异步IO接口中,尽管它既能以同步的方式来使用,又能以异步的方式来使用,这个函数就是lio_listio。该函数提交一系列由一个AIO控制块列表描述的IO请求。
#include
int lio_listio(int mode,struct aiocb *restrict const list[restrict],int nent,struct sigevent *restrict sigev);
//方括号中的restrict是什么意思???
每个AIO控制块中,aio_lio_opcode地段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是被忽略的空操作(LIO_NOP)。
引入POSIX异步操作IO接口的初衷是为实时应用提供一种方法,避免在执行IO操作时阻塞进程。
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。
#include
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.
管道、FIFO以及某些设备(特别是终端和网络)有下列两种性质:
#include
ssize_t readn(int fd,void *buf,size_t nbytes);
ssize_t writen(int fd,void *buf,size_t nbytes);
存储映射IO(memory-mapped IO)能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。这样就可以在不使用read和write的情况下执行IO。
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的。
#include
void *mmap(void *addr,size_t len,int port,int flag,int fd,off_t off);
子进程通过fork继承存储映射区(因为子进程复制父进程地址空间,而存储区映射是该地址空间的一部分)。新程序则不能通过exec继承存储映射区。
mprotect函数可以更改一个现有的映射权限:
#include
void mprotect(void *addr,size_t len,int prot);
prot的合法值与mmap中prot参数一样。注意,地址参数addr的值必须是系统页长的整数倍。
如果共享映射中的页已经修改,那么可以调用msync将该页冲洗到被映射的文件中。msync函数类似于fsync,但用于存储映射区。
#include
void msync(void *addr,size_t len,int flags);
当进程终止时,会自动解除映射区的映射,或者直接调用munmap函数也可以解除映射区。关闭映射存储区使用的文件描述符并不解除映射区。
#include
int munmap(void *addr,size_t len);
munmap并不影响被映射的对象,也就是说,调用munmap并不会使映射区的内容写到磁盘文件上。
本章介绍了很多高级IO功能。