《UNIX 环境高级编程》学习笔记——高级I/O

  • Post author:
  • Post category:其他




非阻塞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版



版权声明:本文为weixin_45243288原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。