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

};

读锁可以共享,写锁不行。

对同一进程而言,在同一文件的同一区域只能同时持有一把锁,新加的锁会替换掉以前的锁。

这里提出一种特殊情况:如果同一文件区域被很多读线程等待,那么等待的写线程可能会饿死…

系统会自动断开或合并相邻的加锁区。

锁的规则

  1. 进程终止时,其建立的锁会被全被释放;
  2. 任何时候关闭一个描述符,通过该描述符可以引用的文件的任何一把锁都被释放;
  3. fork产生的子进程不继承父进程所设置的锁。
  4. 在执行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撤销映射。

注意关闭文件描述符并不能撤销掉映射;

使用内存映射文件操作文件的目的是节省时间。

posted @ 2013-02-15 14:51  生无所息  阅读(204)  评论(0编辑  收藏  举报