本篇总结一些高级的I/O操作,包括记录锁、I/O多路转接、存储映射I/O等。
Contents
- 记录锁
- I/O多路转接
- read和write函数的变种
- 存储映射I/O
记录锁
记录锁可以确保进程在单独地写一个文件。当一个进程正在读或修改文件的某个部分时,记录锁可以阻止其他进程修改同一文件区。记录锁一般是用于数据库等应用。
fcntl 函数提供了记录锁的功能。对于记录锁,fcntl 可以看作:
#include <unistd.h> #include <fcntl.h> /* 记录锁操作 * @return 成功返回依赖于cmd,出错返回-1 */ int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
cmd 命令为:
- F_GETLK :判断 flockptr 指定的锁是否被另一把锁阻塞,若存在则把该锁的信息写入 flockptr 指向的结构中,否则只将 l_type 设置为 F_UNLCK 。
- F_SETLK :设置 flockptr 指定的锁,如果不被允许则出错返回, errno 设为 EAGAIN 。 l_type 为F_UNLCK 时用来清除 flockptr 指定的锁。
- F_SETLKW : F_SETLK 的阻塞版本。按兼容性不被允许创建时,使进程休眠,当可以创建锁或休眠被信号中断时,进程被唤醒。
结构 flock 的定义如下:
struct flock { /* ... */ short l_type; /* 锁类型:F_RDLCK, F_WRLCK, F_UNLCK */ short l_whence; /* l_start的起点:SEEK_SET, SEEK_CUR, SEEK_END */ off_t l_start; /* 相对偏移量 */ off_t l_len; /* 加锁的字节数 */ pid_t l_pid; /* F_GETLK命令时设置为具有阻塞当前进程的锁的进程ID */ /* ... */ };
锁类型有三种,分别是共享的读锁 F_RDLCK 、 独占的写锁 F_WRLCK 和解锁 F_UNLCK 。加读锁时,描述符必须是读打开的;加写锁时,描述符必须是写打开的。不同进程可以共享一个读锁,或由一个进程独占写锁,读锁和写锁是互斥的。同一进程对同一区域重复加锁会替换原来的锁。在设置或释放文件上的锁时,系统会按要求组合或裂开相邻区。
加解锁的区域可以超过文件结尾,但不能从文件开头之前开始。 l_len 为0时表示锁的区域从起点到最大可能的偏移量为止,因此向文件添写的数据都将处于锁的范围内。 l_len 为负值表示指定偏移量之前的字节数。
在文件结尾加锁和解锁时需要特别小心,因为追加写入会导致文件结尾位置的变化。
和线程锁类似,如果两个进程相互等待对方锁定的资源,就会发生死锁。
记录锁的释放规则是:
- 进程终止时,它设置的锁全部释放。
- 关闭描述符时,进程通过该描述符可以引用的文件上的所有该进程设置的锁都被释放。
注意第2条是针对文件,而不是描述符。
记录锁的继承规则是:
- fork 产生的子进程不继承父进程所设置的锁。
- 执行 exec 后新程序继承原执行程序的锁,但如果描述符设置了执行时关闭标志,则关闭该描述符时,锁也被释放。
记录锁分为建议性和强制性的。如果一个数据库的函数库中所有函数以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集为合作进程。对它们建议性锁是可行的,但建议性锁不能阻止对数据库文件有写权限的其他进程进行的写操作,不使用一致方法的这类进程是非合作进程。强制性锁使内核对每个open 、 read 和 write 系统调用都进行检查,看进程对文件的访问是否违背了某个锁。但实际上强制性锁仍可以被绕开。Linux上默认不开启强制性锁的支持。
I/O多路转接
有些情况下需要处理从多个描述符读数据,但因为我们不知道哪些描述符有数据可读,如果使用阻塞I/O显然会造成大量的等待时间,写数据也有同样的问题。
可以使用多进程、多线程、轮询、异步I/O等办法解决,但它们都不是很好的解决办法。
比较好的办法是使用I/O多路转接。先构造一个描述符的列表,然后调用I/O多路转接函数,直到描述符中的一个已经准备好时,函数才返回,返回时告诉进程哪些描述符已经准备好可以进行I/O。
可以用 select 函数执行I/O多路转接。
#include <sys/select.h> /* I/O多路转接 * @return 成功返回准备好的描述符数,超时返回0,出错返回-1 */ int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); /* 置0描述符集的所有位 */ void FD_ZERO(fd_set *set); /* 设置描述符集的指定位 */ void FD_SET(int fd, fd_set *set); /* 清除描述符集的指定位 */ void FD_CLR(int fd, fd_set *set); /* fd在描述符集中返回非0值,否则返回0 */ int FD_ISSET(int fd, fd_set *set);
nfds 为最大描述符加1, select 会在此范围内搜索指定的描述符。
select 中指向描述符集的指针可以为 NULL ,表示不关心。
声明描述符集后,必须先用 FD_ZERO 清除其所有位,然后设置所需的位。 select 会根据测试的结果修改传递的描述符集。从 select 返回后,用 FD_ISSET 测试给定位是否仍设置。
timeout 参数指定愿意等待的时间,有三种情况:
- NULL ,永远等待。有描述符准备好或捕捉到信号则返回。捕捉到信号时返回-1, errno 设置为EINTR 。
- 时间值为0,不等待。测试完指定的描述符后立即返回。
- 时间值不为0,等待指定的时间。有描述符准备好或超时时返回,也可以被捕捉到的信号中断。未超时返回时,会将 timeout 设置为剩余时间。
timeval 和 timespec 结构的定义如下:
struct timeval { long tv_sec; /* 秒数 */ long tv_usec; /* 微秒数 */ }; struct timespec { long tv_sec; /* 秒数 */ long tv_nsec; /* 纳秒数 */ };
描述符准备好的意思是:
- 若对 readfds 中的一个描述符的 read 操作不阻塞,则该描述符是准备好的。
- 若对 writefds 中的一个描述符的 write 操作不阻塞,则该描述符是准备好的。
- 若 exceptfds 中的一个描述符有一个未决异常状态,则该描述符是准备好的。异常状态包括在网络连接上到达的带外数据或在处于数据包模式的伪终端上发生了某些状态。
- 普通文件描述符总是返回准备好。
在描述符上碰到文件结尾处, select 认为该描述符是可读的,然后调用 read 会返回0。
pselect 和 select 的区别是:
- timeout 使用 timespec 结构,提供更细的时间粒度。不改变 timeout 。
- 可使用信号屏蔽字, pselect 会以原子操作安装屏蔽字,在返回时恢复原来的屏蔽字。
poll 函数和 select 类似,但它使用 pollfd 结构数组代替了描述符集。
#include <poll.h> /* I/O多路转接 * @return 成功返回准备好的描述符数,超时返回0,出错返回-1 */ int poll(struct pollfd *fds, nfds_t nfds, int timeout); int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask);
结构 pollfd 的定义如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 请求事件 */ short revents; /* 返回事件 */ };
设置 events 成员来告诉内核对该描述符关心什么,可以设置标志包括:
- POLLIN :不阻塞地可读除高优先级外的数据,等效于 POLLRDNORM|POLLRDBAND 。
- POLLRDNORM :不阻塞地可读普通数据(优先级波段为0)。
- POLLRDBAND :不阻塞地可读非0优先级波段数据。
- POLLPRI :不阻塞地可读高优先级数据。
- POLLOUT :不阻塞地可写普通数据。
- POLLWRNORM :等价于 POLLOUT 。
- POLLWRBAND :不阻塞地可写非0优先级波段数据。
revents 由内核设置,来说明该描述符发生了什么,标志除以上7个外还有:
- POLLERR :已出错。
- POLLHUP :已挂断。
- POLLNVAL :描述符没引用打开文件。
描述符被挂断后就不能再写向该描述符,但仍可能从该描述符读取到数据。
nfds 说明 fds 结构数组中的元素数。
timeout 参数的用法同 select 函数。 ppoll 和 poll 的关系和 select 类似。
read和write函数的变种
readv 和 writev 函数用于一次读写多个非连续缓冲区,也称为散布读和聚集写,即 readv 将读取的数据散布在多个缓冲区中, writev 从多个缓冲区中聚集输出数据。
#include <sys/uio.h> /* 读多个非连续缓冲区 * @return 成功返回已读字节数,出错返回-1 */ ssize_t readv(int fd, const struct iovec *iov, int iovcnt); /* 写多个非连续缓冲区 * @return 成功返回已写字节数,出错返回-1 */ ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
iov 指向 iovec 结构数组,结构 iovec 的定义如下:
struct iovec { void *iov_base; /* 开始地址 */ size_t iov_len; /* 字节数 */ };
iovcnt 说明 iov 结构数组中的元素数。
管道、FIFO、终端和网络等常常由于流量控制或缓冲大小使 read 和 write 操作处理的数据少于指定量,一般要按需多次调用。因为经常遇到这种情况,所以把它们封装为 readn 和 writen 函数。
#include <unistd.h> /* Read "n" bytes from a descriptor */ ssize_t readn(int fd, void *ptr, size_t n) { size_t nleft; ssize_t nread; nleft = n; while (nleft > 0) { if ((nread = read(fd, ptr, nleft)) < 0) { if (nleft == n) return(-1); /* error, return -1 */ else break; /* error, return amount read so far */ } else if (nread == 0) { break; /* EOF */ } nleft -= nread; ptr += nread; } return(n - nleft); /* return >= 0 */ } /* Write "n" bytes to a descriptor */ ssize_t writen(int fd, const void *ptr, size_t n) { size_t nleft; ssize_t nwritten; nleft = n; while (nleft > 0) { if ((nwritten = write(fd, ptr, nleft)) < 0) { if (nleft == n) return(-1); /* error, return -1 */ else break; /* error, return amount written so far */ } else if (nwritten == 0) { break; } nleft -= nwritten; ptr += nwritten; } return(n - nleft); /* return >= 0 */ }
存储映射I/O
存储映射I/O使一个磁盘文件和存储空间中的一个缓冲区建立映射。从缓冲区中取数据,相当于读文件中的相应字节;将数据存入缓冲区,相应字节就自动写入文件。这样就不用使用 read 和 write 函数。
存储映射I/O一般会比较快,而且常常可以简化算法,但是它不能用在网络和终端等设备上。
使用 mmap 函数建立映射,用 munmap 函数解除映射。
#include <sys/mman.h> /* 建立文件和缓冲区的映射 * @return 成功返回映射区的起始地址,出错返回MAP_FAILED */ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); /* 解除映射 * @return 成功返回0,出错返回-1 */ int munmap(void *addr, size_t length);
参数说明:
- addr
- 指定映射存储区的起始地址,通常设为0来由系统选择。
- len
- 映射的字节数。
- prot
-
指定对映射存储区的保护要求,可以为下列标识的按位或,但保护要求不能超过文件 open 模式访问权限。
- PROT_READ :映射区可读。
- PROT_WRITE :映射区可写。
- PROT_EXEC :映射区可执行。
- PROT_NONE :映射区不可访问。
- flags
-
有三种可取值,其中 MAP_SHARED 和 MAP_PRIVATE 必须有且只有一个:
- MAP_FIXED :返回值必须为 addr ,这不利于可移植性,所以不建议使用。不指定它时, addr只被视作建议值。
- MAP_SHARED :对映射区的存储操作相当于对文件的 write 。
- MAP_PRIVATE :对映射区的存储操作会创建文件的一个私有副本,后面对映射区的引用都引用该副本。
- fd
- 要被映射文件的描述符。
- offset
- 映射字节在文件中的起始偏移量。
offset 和 addr 应该是系统页长的整数倍。
调用 fork 后,子进程继承映射存储区,调用 exec 后,新程序不继承此映射存储区。
进程终止时或调用 munmap 函数后,映射存储区会被自动解除映射,但关闭文件描述符则不解除映射。
munmap 不处理将映射区数据写到文件的工作,对 MAP_SHARED ,更新按内核虚拟存储算法自动进行,MAP_PRIVATE 在解除映射后,映射区的修改会被丢弃。
mprotect 函数可以更改现有的映射存储区的权限。
#include <sys/mman.h> /* 更改现有的映射存储区的权限 * @return 成功返回0,出错返回-1 */ int mprotect(const void *addr, size_t len, int prot);
函数的参数和 mmap 用法相同。
可以用 msync 将映射存储区中已修改的页刷新到被映射的文件中。
#include <sys/mman.h> /* 刷新映射存储区中已修改的页 * @return 成功返回0,出错返回-1 */ int msync(void *addr, size_t length, int flags);
flags 参数必须要包含 MS_SYNC 和 MS_ASYNC 中的一个,前者使函数等待写操作的完成,后者则可简化被写页的调度。
例:
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include "error.h" #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) int main(int argc, char *argv[]) { int fdin, fdout; void *src, *dst; struct stat statbuf; if (argc != 3) err_quit("usage: %s <fromfile> <tofile>", argv[0]); if ((fdin = open(argv[1], O_RDONLY)) < 0) err_sys("can't open %s for reading", argv[1]); if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0) err_sys("can't creat %s for writing", argv[2]); if (fstat(fdin, &statbuf) < 0) /* need size of input file */ err_sys("fstat error"); /* set size of output file */ if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1) err_sys("lseek error"); if (write(fdout, "", 1) != 1) err_sys("write error"); if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) err_sys("mmap error for input"); if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED) err_sys("mmap error for output"); memcpy(dst, src, statbuf.st_size); /* does the file copy */ exit(0); }