1.非阻塞I/O
非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。对于一个给定的描述符有两种方法对其指定非阻塞I / O:
(1) 如果是调用open以获得该描述符,则可指定O_NONBLOCK标志
(2) 对于已经打开的一个描述符,则可调用fcntl打开O)NONBLOCK文件状态标志
2.记录锁
记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区
1).fcntl记录锁
下面给出fcntl函数原型
#include <fcntl.h> int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */); //如果成功根据cmd返回,错误返回-1。
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。
- F_GETLK判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况除了将l_type设置为F_UNLCK之外,flockptr所描述的其他信息都不变。
- F_SETLK和F_SETLKW企图建立一把锁。F_SETLK和F_SETLKW的区别是F_SETLKW是F_SETLK的阻塞版本。如果存在其他锁,调用的进程就被阻塞直道捕捉到信号
第三个参数(称其为flockptr)是一个指向flock结构的指针
struct flock { short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */ off_t l_start; /* offset in bytes, relative to l_whence */ short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */ off_t l_len; /* length, in bytes; 0 means lock to EOF */ pid_t l_pid; /* returned with F_GETLK */ };
flock结构说明:
- 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域),这是由 l_type决定的。
- 要加锁或解锁区域的起始字节偏移量,这是由l_statt和l_whence两者决定。
- 区域的字节长度,由l_len表示。
- 具有能阻塞当前进程的锁,其持有的ID存放在l_pid中。
- 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于的范围。
- 如果想锁住整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,1_len说明为0。
- 还要注意的是,对文件加共享读锁时文件应以只读的方式打开,对文件加独占写锁时文件应以只读的方式打开。
2).锁的隐含继承和释放
关于记录锁的自动继承和释放有三条规则:
a.锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)
b. 由fork产生的子程序不继承父进程所设置的锁
c. 在执行exec后,新程序可以继承原执行程序的锁。
4).FreeBSD的实现
下图显示了open、fork以及dup后的数据结构,其他解释详见APUE
4).在文件尾加锁
在接近文件尾端加锁和解锁时要特别小心。大多数实现是按照把l_whence的SEEK_CUR或SEEK_END的值,用l_start以及文件当前位置或当前长度得到绝对文件偏移量
5).建议性锁和强制性锁
如果这些函数是唯一的用来存取数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件有写许可权的任何其他进程写数据库文件。不使用协同一致的方法(数据库存取例程库)来存取数据库的进程是一个非合作进程。
强制性锁机制中,内核对每一个open、read和write都要检查调用进程对正在存取的文件是否违背了某一把锁的作用。
6).实例:
加锁和解锁一个文件区域的函数
#include <fcntl.h> int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len) { struct flock lock; lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */ lock.l_start = offset; /* byte offset, relative to l_whence */ lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ lock.l_len = len; /* #bytes (0 means to EOF) */ return(fcntl(fd, cmd, &lock)); }
文件整体上锁
#include <unistd.h> #include <fcntl.h> int lockfile(int fd) { struct flock fl; fl.l_type = F_WRLCK; fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; return(fcntl(fd, F_SETLK, &fl)); }
3.STREAMS
STREAMS是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法。
STREAMS的所有输入和输出都基于消息。STREAMS首和用户进程使用read、write、getmsg、getpmsg、putmsg和putpmsg交换消息,详见APUE
4.I/O多路转接
1).select和pselect函数
select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:我们所关注的描述符;对于每个描述符我们所关心的状态,以及我们愿意等待的时间。从select返回时,内核告诉我们:以准备好的描述符的数量,对于读、写或异常这三个状态中的每一个,那些描述符已经准备好。
#include <sys/select.h> int select (int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,
fd_set *restrict exceptfds, struct timeval *restrict tvptr); //返回准备好的描述符的计数,超时返回0,错误返回-1。
第一个参数maxfdp1的意思是“最大描述符加1”。也可将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常数,它说明了最大的描述符数(经常是1024)。如果将第三个参数设置为我们所关注的最大描述符编号值加一,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。
中间的三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读(readfds)、可写(writefd)或处于异常条件(wxcepfds)的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种结构相当于一个描述符的数组,它为每个可能的描述符设置1位。
select的中间三个参数中的任意一个或全部都可以是空指针,这表示对相应状态不关系。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。其等待时间可以小于1秒。
tvptr指定最后等待的时间,它的结构是:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* and microseconds */ };
a. tvptr==NULL:永远等待。如果捕捉到一个信号则中断此无限等待。当所指定的描述符中的一个已经准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR.
b. tvptr->tv_sec==0&&tvptr_usec==0 完全不等待。测试所有的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。
c. tvptr->tv_sec!=0||tvptr_usec!=0 等待指定的秒数或微秒数。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是0
select有三个可能的返回值。
a. 返回值-1表示出错。这是可能发生的,例如在所指定的描述符都没有准备好时捕捉到一个信号。
b. 返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。
c. 返回一个正值说明了已经准备好的描述符数,在这种情况下,三个描述符集中仍旧打开的位是对应于已准备好的描述符位。
对fgset数据类型可以进行的处理是: (a)分配一个这种类型的变量, (b)将这种类型的一个变量赋与同类型的另一个变量,或(c)对于这种类型的变量使用下列四个宏:
#include <sys/select.h> int FD_ISSET(int fd, fd_set *fdset); //如果fd被设置返回非0,否则返回0。 void FD_CLR(int fd, fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset);
这些接口可以作为函数或宏实现。调用FD_ZERO将一个fd_set变量的所有位置为0位。调用FD_SET设置一个fd_set变量的指定位,调用FD_CLR则将一指定位清除。最后,调用FD_ISSET测试一指定位是否设置
POSIX.1也定义了select的变体,称为pselect。
#include <sys/select.h> int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict wrtefds, fd_set *restrict exceptfds,
const struct timespec *restrict tsptr, const sigset_t *restrict sigmask); //返回准备好的描述符的计数,到时返回0,错误返回-1
pselect函数和select一样,除了以下的例外:
a. select的计时值被一个timeval结构体指明,但是对于pselect,一个timespec结构体被使用。作为秒和微秒的替代,timespec结构体以秒和纳秒表示计时值。这提供了更高精度的计时,如果平台支持这种粒度的话。
b.pselect的计时值被声明为const,我们被保证它的值不会因为pselect的调用被改变。
c.对于pselect可以使用一可选择的信号屏蔽字。
5.poll函数
poll函数类似于select,但是其调用形式则有所不同
include <poll.h> int poll(struct poolfd fdarray[], nfds_t nfds, int timeout); //返回准备好的描述符的计数,到时返回0,错误返回-1。
与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
struct pollfd { int fd; /* file descriptor to check, or <0 to ignore */ short events; /* event of interest on fd */ short revents; /* event that occurred on fd */ };
在fdarray数组里的元素数由nfds指定。
poll的最后参数指明我们想要等待多久。和select一样,有三种情况
a.timeout == -1:永远等待 b.timeout == 0:不等待 c.timeout > 0
6.readv和writev函数
readv和writev函数用于在一个函数调用中读、写多个非连续缓存。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。
#include <sys/uio.h> ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); ssize_t writev(int filedes, const struct iovec *iov, int iovcnt); //两者都返回读或写的字节数,错误返回-1。
这两个函数的第二个参数是指向iovec结构数组的一个指针:
struct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */ };
显示了readv和writev的参数和iovec结构之间的关系
writev以顺序iov[0], iov[1]至iov[iovcnt-1] 从缓存(缓存是分散的)中聚集输出数据。writev返回输出的字节总数,它应等于所有缓存长度之和。
readv则将读入的数据按上述同样顺序散布到缓存(缓存是分散的)中。readv总是先填满一个缓存,然后再填写下一个。readv返回读得的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
7.readn和writen函数
下面两个函数readn和writen的功能是读、写指定的N字节数据,并处理返回值小于要求值的情况。这两个函数只是按需多次调用read和write直至读、写了N字节数据。
#include "apue.h" ssize_t readn(int filedes, void *buf, size_t nbytes); ssize_t writen(int filedes, void *buf, size_t nbytes); //两者返回读或写的字节数,错误返回-1。
在要将数据写到上面提到的设备上时,就可调用writen,但是仅当先就知道要接收数据的数量时,才调用readn(通常,调用read以接收来自这些设备的数据)。
8.存储映射I/O
存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的
#include <sys/mman.h> void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off); //成功返回被映射区域的开始地址,错误返回MAP_FAILED。
addr参数用于指定映射存储区的起始地址,len是映射的字节数,filedes指定要被映射文件的描述符,off是要映射字节在文件中的起始位移量,prot参数指定映射区域的保护,此函数的返回地址是:该映射区的起始地址。
prot参数取值:
调用mprotect来改变一个已有映射上的权限。
#include <sys/mman.h> int mprotect(void *addr, size_t len, int prot); //成功返回0,错误返回-1。
调用msync来冲洗对被映射文件的改变
#include <sys/mman.h> int msync(void *addr, size_t len, int flags); //成功返回0,错误返回-1。
调用了munmap之后,存储映射区就被自动去除。
#include <sys/mman.h> int munmap(caddr_t addr, size_t len); //成功返回0,错误返回-1。