Apue.2e Chapter14 Advanced I/O
本章主要讲解高级I/O,是以后各章学习的基础。
非阻塞I/O:指定文件打开方式时,oflag中添加O_NONBLOCK标志,或者对已经打开的文件描述符使用fcntl更换文件的打开标志。
记录锁
record locking的作用是:可以锁定文件中的一部分,阻止其他进程修改同一文件区。
使用fcntl函数完成该功能,
int fcntl(int filedes, int cmd, …/*struct flock *flockptr here*/);
cmd可以是F_GETLK(填充flockptr指向的区域,如果无锁则设置l_type为F_UNLCK), F_SETLK(设置/清除锁,如果不能,返回-1,设置errno), F_SETFKW(w表示wait,阻塞版本)
struct flock{
short l_type; //F_RDCLK, F_WRLCK, F_UNLCK
off_t l_start;//offset in bytes, relative to l_whence
short l_whence;// SEEK_SET, SEEK_CUR, SEEK_END
off_t l_len; //length, in bytes; 0 means lock to EOF
pid_t l_pid; //returned with F_GETLK
};
读锁可以共享,写锁不行。
对同一进程而言,在同一文件的同一区域只能同时持有一把锁,新加的锁会替换掉以前的锁。
这里提出一种特殊情况:如果同一文件区域被很多读线程等待,那么等待的写线程可能会饿死…
系统会自动断开或合并相邻的加锁区。
锁的规则
- 进程终止时,其建立的锁会被全被释放;
- 任何时候关闭一个描述符,通过该描述符可以引用的文件的任何一把锁都被释放;
- fork产生的子进程不继承父进程所设置的锁。
- 在执行exec后,新程序可以继承原执行程序的锁,但如果文件描述符打开时设置了close-on-exec标志,则由于规则2,所有锁都会被释放。
在文件尾端加锁:注意内核会将相对偏移量换算成绝对偏移量。
建议性锁和强制性锁:前者不能阻止其他有写权限的进程对文件进行随意的写操作,后者则强迫内核对每次r/w系统调用都进行检查,可以通过setgid并关闭S_IXGRP的方法打开强制性锁(for linux)。强制性锁也有一些bug,甚至可以被设法避开(这块可能outdate了)
由于大部分编辑器并不使用强制性记录锁,甚至大部分会在读取文件后关闭文件,所以事实上想要阻止多进程/用户编辑同一文件基本是不可能的。【现在应该可以通过版本控制系统解决类似的问题】。
STREAMS
这块比较难理解。
streams是在用户进程和设备驱动之间构建了一条全双工通路,stream head是系统调用api,其下可以压入各个处理模块,从driver到stream head称为逆流,反之称为顺流,streams作为内核一部分被执行,可以使用文件访问函数访问streams,所有的STREAMS设备都是字符设备文件。
streams的I/O都是基于消息机制的,
struct strbuf{
int maxlen; //size of buffer
int len; //number of bytes currently in buffer
char *buf; //pointer to buffer
};
控制信息和数据都是存放在上述数据结构中的,len=-1,说明没有信息,len=0是允许的。
对于用户层程序员而言,只涉及到三种消息类型,即:
M_DATA(I/O USER DATA), M_PROTO(protocol control info),M_PCPROTO(high priority protocol info)
#include <stropts.h>
int putmsg(int filedes,const struct strbuf *ctlptr, const struct strbuf *dataptr,int flag);
int putpmsg(int filedes, const struct strbuf *ctlptr, const struct strbuf *dataptr, int band, int flag); //p指的是priority,可以指定优先级波段
优先级分为三种:
高,(最高,只能有一个)
优先级波段指定(1~255);
普通(0)
流系统的处理函数主要使用ioctl,可执行40+中不同的操作,可以man 7 streamio查看,这里列举一些例子:
#include <stropts.h>
int isastream(int filedes); //成功返回1,失败返回0
测试环境(debian6 stable)下不支持streams相关操作…虽然可以编译通过(因为头文件中有函数原型),但是没有正常的实现。
I/O多路转接
如果需要同时从多个文件读取,可以使用的方法很多,这里给出一种专门用于解决此类问题的技术,及 I/O multiplexing。
其机制类似于表驱动,函数查询等待一个表中感兴趣的I/O单元类,当该类单元中有一个已经准备好I/O时,函数返回并告之等待进程那些已经准备ok,可以进行I/O动作。
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict tvptr); //tvptr表示超时时间;fd_set表示描述符集合,说明了处于可读、可写和异常状态的描述集指针;maxfdp1是最大描述符+1(后面三个集合中的)
操作描述符集的函数(或宏):
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); //测试某指定位是否设置
void FD_CLR(int fd, fd_set *fdset); //清零某个位
void FD_SET(int fd,fd_set *fdset); //将某个位置位
void FD_ZERO(fd_set *fdset); //清零所有位
如果3个描述符集参数都是NULL,原函数相当于一个定时器。
select返回:-1,出错,不修改任何描述符集;
返回0:没有描述符准备好,所有描述符集被清0;
正返回值:已经准备好的描述符数目;后面三个描述符集中的描述符数目之和;
类似的有pselect函数,不同的是可以设置信号屏蔽字;
poll:
#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds, int timeout);
poll不是以状态为分类构造描述符集合,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的状态;
struct pollfd{
int fd;
short events; //events of interest on fd
short revents; //events that occurred on fd
};
ntfds说明前面数组的元素数;timeout指定超时时间;
events里面有POLLUP标志,标示已挂断,挂断后不能写入新数据,但可以读取以前的数据。
挂断和文件结束是不同的,后者并不会触发任何错误标示,只是read返回0而已。
select是否是可再启动的系统调用,取决于系统。
异步I/O
由于信号时以进程为单位的,因此如果要同时对几个描述符进行异步I/O,在接收到信号时并不知道该信号对应哪个描述符。
在系统V中,异步I/O是通过STREAMS机制实现的,其信号是SIGPOLL;
BSD系统,异步I/O是SIGIO和SIGURG两个信号的组合,前者是通用的,后者仅表示进程在网络连接上到达了带外的数据;实现方法是使用fcntl指定处理进程,并在进程中设置好信号处理函数,然后还需要设置文件状态标志为O_ASYNC;
readv和writev用于在一次函数调用中读、写多个非连续缓冲区,返回读入/输出的字节数。
对于管道、FIFO以及终端、网络等设备,一次read|write操作可能少于指定的字节数,因此可能需要多次I/O才能保证数据被完全处理。
文中实现了readn和writen函数用来自动完成这一工作(就是一个循环)。
存储映射I/O
使文件与存储空间中的一个缓冲区相映射,这样读缓冲区就是读文件,写缓冲区就是写文件;
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
addr:指定存储区起始地址,一般设置为0,由系统自己选择;
len:文件长度
prot:对映射存储区的读写保护要求(不能高于文件描述符的打开权限);
flag:一些属性,重要的有MAP_SHARED和MAP_PRIVATE,这两个标志是互斥的,前者表示修改缓冲区会被实际写入文件,后者表示操作的其实是副本,一切修改都会被放弃;
off:起始偏移量,一般也设置为0;如果不是0,必须设置成为_SC_PAGESIZE的整数倍;
相关信号:SIGSEGV和SIGBUS,前者一般指示进程试图访问对他不可用的存储区;如果访问映射区的某个部分,而访问时这个部分实际上不存在,则产生SIGBUS信号;
fork后,子进程继承存储映射区。
调用mprotect可以更改一个现存存储区的权限;
可以使用msync重新修改的脏页面到文件中;
使用munmap撤销映射。
注意关闭文件描述符并不能撤销掉映射;
使用内存映射文件操作文件的目的是节省时间。