APUE 第14章 高级I/O

来源:互联网 发布:游戏常用的算法 编辑:程序博客网 时间:2024/06/05 19:13

第14章 高级I/O

非阻塞IO

我们可以发出open,read,write这样的IO操作,并使它们永远不会阻塞,如果无法做到,则立即返回出错。

两种方法获得非阻塞IO:
1. open打开使指定O_NONBLOCK;
2. 对于一个已经打开的描述符,调用fcntl增加上述标志位。

记录锁

记录锁(record locking)的名称是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念。更适合的术语是字节范围锁(byte-range locking),因为它锁定的只是文件的一个区域。

这种功能用于支持对数据库的维护。

int fcntl(int fd, int cmd, struct flock *lock);struct flock {    short l_type;/*F_RDLCK, F_WRLCK, or F_UNLCK*/    off_t l_start;/*相对于l_whence的偏移值,字节为单位*/    short l_whence;/*从哪里开始:SEEK_SET, SEEK_CUR, or SEEK_END*/    off_t l_len;/*长度, 字节为单位; 0 意味着缩到文件结尾*/    pid_t l_pid;/*returned with F_GETLK*/};

结构体描述:

  • l_type:锁类型: F_RDLCK(读共享锁), F_WRLCK(写互斥锁),和F_UNLCK(对一个区域解锁)
  • l_start:锁开始:相对于l_whence要锁或者解锁的区域开始位置
  • l_whence:锁位置(l_whence)
  • l_len:锁长度: 要锁的长度,字节计数
    -l_pid:锁拥有者:记录锁的拥有进程ID,这个进程可以阻塞当前进程,仅F_GETLK形式返回

对于锁区域要注意的几点:

  1. 锁可以开始或者超过文件当前结束位置,但是不可以开始或者超过文件的开始位置
  2. 如果l_len为0,意味着锁的区域为可以到达的最大文件偏移位置。这个类型,可以让我们锁住一个文件的任意开始位置,结束的区域可以到达任意的文件结尾,并且以append方式追加文件时,也会同样上锁。
  3. 如果要锁住整个文件,设置l_start 和 l_whence为文件的开始位置(l_start为0 l_whence 为 SEEK_SET ),并且l_len为0。
  4. 如果有多个读共享锁(l_type of F_RDLCK),其他的读共享锁可以接受,但是写互斥锁(type ofF_WRLCK)拒绝
  5. 如果有一个写互斥锁(type ofF_WRLCK),其他的读共享锁(l_type of F_RDLCK)拒绝,其他的写互斥锁拒绝。
  6. 如果要取得读锁,这个文件描述符必须被打开可以去读;如果要或者写锁,这个文件的描述符必须可以被打开可以去写。

锁的兼容性规则只对不同进程有效,同一进程对自己已加锁的区间再加锁,则会替换旧锁。

fcntl的cmd参数

  • F_GETLK:判断flockptr描述的锁是否被其他的锁阻塞。
  • F_SETLK :设置flockptr描述的锁,如果兼容性规则阻止,则出错返回。
  • F_SETLKW:对应着F_GETLK的可以阻塞的版本。w意味着wait

系统可以按要求组合或分裂相邻区。

锁的隐含继承和释放

  1. 锁与进程和文件两方面有关:
    a、当一个进程终止时,它所建立的锁全部释放;(即 进程退出,文件锁自动释放)
    b、任何时候关闭一个描述符时,则该进程通过这一”描述符可以引用的文件”上的任何一把锁都释放。(即 关闭文件,文件锁自动释放),该进程设置的该文件相关所有锁都释放,
  2. 由fork产生的子进程不继承父进程所设置的锁。(文件锁不能被继承):
    这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另外一个进程,对于从父进程处继承过来的任一描述符,子进程需要调用fcntl才能获得它自己的锁。
  3. 在执行exec后,新程序可以继承原执行程序的锁。(EXEC文件锁被继承),执行exec后,其实是用当前进程的进程实体替换原进程的进程实体。
    如果对一个文件描述符设置了执行时关闭,当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁。

建议性锁和强制性锁

  • 建议锁又称协同锁。对于这种类型的锁,内核只是提供加减锁以及检测是否加锁的操作,但是不提供锁的控制与协调工作。也就是说,如果应用程序对某个文件进行操作时,没有检测是否加锁或者无视加锁而直接向文件写入数据,内核是不会加以阻拦控制的。因此,建议锁,不能阻止进程对文件的操作,而只能依赖于大家自觉的去检测是否加锁然后约束自己的行为;多数 Unix 和类 Unix 操 作系统使用建议型锁,有些也使用强制型锁或兼而有之。
  • 强制锁,是内核的文件锁。每个对文件操作时,例如执行open、read、write等操作时,OS内部检测该文件是否被加了强制锁,如果加锁导致这些文件操作失败。也就是内核强制应用程序来遵守游戏规则;微软的操作系统往往使用的是强制型锁。
int select( int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);/*maxfdp1:是一个整数值,是指集合中所有文件描述符的范围,即3个集合中所有文件描述符的最大值加1,不能错!readfds:(可选)指针,指向一组等待可读性检查的描述符。writefds:(可选)指针,指向一组等待可写性检查的描述符。exceptfds:(可选)指针,指向一组等待错误检查的描述符。timeout:select()最多等待时间,设为NULL则阻塞,设为0则不阻塞,设为正数则等待指定时间。*/
readfds, writefds, exceptfds可以实现为位图。利用一组函数来设置、清除、测试指定fd。 返回时,-1表示出错,一般是因为接收到某信号;0表示没有描述符准备好,此时3个集合都置0;一个正数表示准备好的3个集合的数量之和,此时3个集合将准备好的位置1。注意,如果同一描述符出现在多个集合中,则累加计数。

一个描述符碰到文件尾端,它仍是可读的。
一个描述符是否阻塞与select是否阻塞没有关系

pselect变体的区别,使用timespec结构可以实现更精准的超时时间(它以秒和纳秒表示,而timeval以秒和微秒表示);它的超时时间是const,所以不能改变时间值;它还可以使用信号屏蔽字。poll提供的功能与select类似:
int poll(struct pollfd fd[], nfds_t nfds, int timeout);    参数:
  1. 第一个参数:一个结构数组,struct pollfd结构如下:
struct pollfd{  int fd;              //文件描述符 short events;    //请求的事件 short revents;   //返回的事件};

events和revents是通过对代表各种事件的标志进行逻辑或运算构建而成的。events包括要监视的事件,poll用已经发生的事件填充revents。poll函数通过在revents中设置标志肌肤POLLHUP、POLLERR和POLLNVAL来反映相关条件的存在。不需要在events中对于这些标志符相关的比特位进行设置。如果fd小于0, 则events字段被忽略,而revents被置为0.标准中没有说明如何处理文件结束。文件结束可以通过revents的标识符POLLHUN或返回0字节的常规读操作来传达。即使POLLIN或POLLRDNORM指出还有数据要读,POLLHUP也可能会被设置。因此,应该在错误检验之前处理正常的读操作。
2. 第二个参数nfds:要监视的描述符的数目。
3. 最后一个参数timeout:是一个用毫秒表示的时间,是指定poll在返回前没有接收事件时应该等待的时间。如果 它的值为-1,poll就永远都不会超时。如果整数值为32个比特,那么最大的超时周期大约是30分钟。

然而,select和poll都不是线程安全的,显而易见。

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。

用readv,writev进行散布读和聚集写。

管道、FIFO以及某些设备(特别是终端和网络)有以下两种性质:
1. 一次read操作返回的数据少于要求的数据
2. 一次write操作的返回值少于指定输出的值
利用readn, writen将按需地多次调用read,write直至完成。

存储映射I/O是一种基于内存区域的高级I/O操作,它将磁盘文件与进程地址空间中的一个内存区域相映射。当从这段内存中读数据时,就相当于读磁盘文件中的数据,将数据写入这段内存时,则相当于将数据直接写入磁盘文件。这样就可以在不使用基本I/O操作函数read和write的情况下执行I/O操作。

实现存储映射I/O的核心操作是通过mmap系统调用将一个给定的磁盘文件映射到一个存储区域中

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

prot用来指定对映射区域的保护要求,但是它的保护范围不能超过文件open时指定的打开权限。比如以只读(PROT_READ)方式打开一个文件,那么以读写(PROT_READ|PROT_WRITE)方式保护内存区域是不合法的。flags用来指定内存区域的多种属性,两个典型的取值是MAP_SHARED和MAP_PRIVATE。MAP_SHARED标志指定了进程对内存区域的修改会影响到映射文件,且多个共享该映射区的进程都可以看见。而当对flags指定MAP_PRIVATE时,进程会为该映射内存区域创建一个私有副本,对该内存区的所有操作都是在这个副本上进行的,此时对内存区域的修改并不会影响到映射文件。

off的值和addr的值通常被要求是系统虚拟存储页长度的倍数,将两者都指定为0可以省去这一麻烦而交由系统处理。

mprotect()用于更改一个现有映射的权限,msync()用于将页冲洗到被映射的文件中,可以选择同步或异步方式,munmap()用于解除一个映射,但它并不能影响被映射的对象。

原创粉丝点击