UNIX环境高级编程——高级I/O
非阻塞I/O
非阻塞I/O使我们可以发出 open、read 和 write 这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。
对于一个给定的描述符,有两种为其指定非阻塞I/O的方法。
- (1)如果调用 open 获得描述符,则可指定 O_NONBLOCK 标志。
- (2)对已经打开描述符,调用 fcntl 打开O_NONBLOCK文件标志。
非阻塞在I/O,会立即返回;通过返回结果可获知是否正确完成信息。
记录锁
记录锁的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。
fcntl记录锁
#include <fcntl.h>
int fcntl(
int fd,
// F_GETLK/F_SETLK/F_SETLKW
// cmd为F_GETLK,参数3-flock*是一个值-结果参数
int cmd,
...);
返回值:若成功,依赖于 cmd ,否则,返回-1
struct flock
{
// F_RDLCK/F_WRLCK/F_UNLCK
short l_type;
// SEEK_SET/SEEK_CUR/SEEK_END
short l_whence;
// start offset
off_t l_start;
off_t l_len;
// 返回持有此锁的进程的ID
pid_t l_pid;
};
调用进程已经持有文件A区间A的一个锁后,希望再对文件A区间A加另一个锁,处理方式是直接用新锁替换旧锁(进程对文件A区间A只加一个锁,锁为最后调用fcntl施加的那个)。
锁的隐含继承和释放
关于记录锁的自动继承和释放有3条规则。
-
(1)锁与进程和文件两者相关联。
这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重则不太明显,无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。 -
(2)由fork产生的子进程不继承父进程所设置的锁。
意味着,若一个进程得到一把锁,然后调用 fork ,那么对于父进程获得的锁而言,子进程被视为另一个进程。
对于通过 fork 从父进程处继承过来的描述符,子进程需要调用 fcntl 才能获得它自己的锁。 -
(3)在执行 exec 后,新程序可以继承原执行程序的锁。
注意,如果对于各文件描述符设置了执行时关闭标志,那么当作为 exec 的一部分关闭该文件描述符时,将释放相应文件的所有锁。
FreeBSD
记录锁通过文件+进程来标识。
在父进程中,关闭 fd1,fd2 或 fd3 任一个,都将释放由父进程设置的写锁。
在文件尾端加锁
上面假设首个write执行时,文件位置在尾部,如希望解除追加的第一个字节的锁,un_lock应在参数2传入-1来找到正确位置(文件尾/当前位置随着文件读写而动态变化)。
建议性锁和强制性锁
所谓建议性锁指的是,我们通过fcntl对文件A区域A1进行加锁。
后续其他进程对文件A区域A1进行fcntl时,会参考此区域已经加锁信息来决定操作是否可行;其他进程若未执行fcntl,直接对文件A区域A1执行read、write则,此区域已经加锁信息,对read、write无影响。
所谓强制性锁指的是,我们通过fcntl对文件A区域A1进行加锁。
后续其他进程对文件A区域A1进行read、write、open、fcntl 时均要参考此区域已经加锁信息来决定操作是否可行。
强制性锁对应于互斥锁。
适合于所有使用锁定文件的用户均遵循一致的规则即访问文件前,先进行锁定,再访问,访问后释放锁定的模式来进行进程间资源共享。
I/O多路转接
函数 select 和 pselect
传给 select 的参数告诉内核:
- 我们所关心的描述符;
- 对于每个描述符我们所关系的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符,是否关心一个给定描述符的异常条件);
- 愿意等待多长时间(可以永远等待、等待一个固定的时间或者根本不等待)。
从 select 返回时,内核告诉我们:
- 已准备号的描述符的总数量;
- 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好。
#include <sys/select.h>
// < 0/=0/>0 数值是3个集合中准备好描述符个数之和
int select(
// 三个描述符集合中最大描述符+1
int maxfdp1,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict exceptfds,
// NULL:
// 直到某些描述符已经准备好
// 字段tv_sec,tv_usec均为0:
// 立即返回
// 指定等待时间:
struct timeval *restrict tvptr);// 秒和微秒
对于 fd_set 数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或堆这种类型的变量使用下列4个函数中的一个。
#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);
读准备好,对描述符执行read不会阻塞即为读准备好。
写准备好,对描述符执行write不会阻塞即为写准备好。
函数 poll
#include <poll.h>
int poll(
struct pollfd fdarray[],
nfds_t nfds,
// -1/0/>0
int nsecs);
返回值:准备就绪的描述符数目;若超时,返回;若出错,返回-1
struct pollfd
{
int fd;
short events;
short revents;
};
函数 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);
两个函数的返回值:已读或已写的字节数;若出错,返回-1
struct iovec
{
// 缓存开始地址
void *iov_base;
// 缓存大小
size_t iov_len;
};
writev 函数从缓冲区中聚集输出数据的顺序是: iov[0]、iov[1] 直至 iov[iovcnt-1] 。
writev 返回输出的字节总数,通常应等于所有缓冲区长度之和。
readv 函数则将读入的数据按上述通用顺序散布到缓冲区中。
readv 总数先填满一个缓冲区,然后再填写下一个。
readv 返回读到的字节总数。如果遇到文件尾端,已无数据可读,则返回0 。
存储映射 I/O
存储映射 I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。
首先告诉内核一个给定的文件映射到一个存储区域中。这是由 mmap 函数实现的。
#include <sys/mman.h>
void* mmap(
void* addr,
size_t len,
// PROT_READ/PROT_WRITE/PROT_EXEC/PROT_NONE
// 但保护要求不能超过文件open模式访问权限
int prot,
// MAP_FIXED
// 返回值需要等于addr。未指定时,即使addr有值,也仅是一个建议值
// MAP_SHARED/MAP_PRIVATE
int flag,
int fd,
off_t off);
返回值:若成功,返回映射区的起始地址;若出错,返回 MAP_FALLED
调用 mprotect 可以更改一个现有映射的权限。
#include <sys/mman.h>
int mprotect(
// 需为系统页长倍数
void *addr,
size_t len,
int prot);
返回值:若成功,返回0;若出错,返回-1
共享映射的内存区域对虚拟内存区域的写直接写到关联的磁盘文件。
对所有共享映射进程可见。
此调用让内存修改页立即写回磁盘。
如果共享映射中的页已修改,那么可以调用 msync 将页冲洗到被映射的文件中。
msync 函数类似于 fsync ,但作用域存储映射区。
#include <sys/mman.h>
int msync(
void *addr,
size_t len,
// 二选一
// MS_ASYNC
// MS_SYNC
int flags);
返回值:若成功,返回0;若出错,返回-1
当进程终止时,会自动解除存储映射区的映射,或者直接调用 munmap 函数也可以解除映射区,
关闭映射存储区时使用的文件描述符并不解除映射区。
#include <sys/mman.h>
int munmap(void *addr, size_t len);
回值:若成功,返回0;若出错,返回-1
学习参考资料:
《UNIX 环境高级编程》第3版