UNIX环境高级编程 高级I/O

 

 

相关函数列表

//调用open函数时指定O_NONBLOCK参数即可打开非阻塞I/O

//记录锁
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

//锁记录的 flock结构体
struct flock {
    short l_type;         //希望加锁的类型(读锁,写锁,解锁)
    short l_whence;    //要加锁的偏移类型SEEK_SET,SEEK_CUR,SEEK_END
    off_t  l_start;         //加锁的起始字节
    off_t  l_len             //区域的字节长度
    pid_t l_pid;            //进程的ID持有的锁能阻塞当前进程(仅由F_GETLK返回)
};


//多路转换函数
//下面参数分别是最大文件描述符+1,读,写,异常fd集合,超时时间
#include <sys/select.h>
int select(int maxdfpl, fd_set *restrict readfds, fd_set *restrict 
     writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);

//tvptr的三种情况
//1.tvptr==NULL 永远等待,如果捕捉到一个信号则中断此无限期等待
//2.tvptr->tv_sec==0 && tvptr->tv_usec==0
//   根本不等待,测试所有指定的描述符并立即返回,这是轮询系统找到多个描述
//   符状态而不足赛select函数的方法
//3.tvptr->tv_sec!=0 && tvptr->tv_usec!=0
//   等待指定的秒数和微妙数

//测试和设置标志位函数
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset);     //测试集合中的一个指定位是否打开
int FD_CLR(int fd, fd_set *fdset);  //可以清除一位
int FD_SET(int fd, fd_set *fdset);  //开启描述符集中的一位
int FD_ZERO(fd_set *fdset);         //将fd_set变量的所有位都设置为0

//select的变体
#include <sys/select.h>
int pselect(int maxfdpl, fd_set *restrict readfds, fd_set *restrict
  writefds, fd_set *restrict exceptfds, const struct timespec *restrict
  tsptr, const sigsset_t *restrict sigmask);
//pselect 和 select的几个区别
//1.select的超时时间用timeval结构指定,而pselect用timespec指定可以
//  提供更精准的超时时间
//2.pselect的超时值为声明为const,保证不会再改变
//3.pselect可使用可选信号屏蔽字,若sigmask为NULL,那么在与信号有关的
//方面和select相同。否则sigmask指向一信号屏蔽字,在调用pselect时,以
//原子操作的方式安装该信号屏蔽字

//poll函数类似于select,但是poll不是为每个条件(可读,可写和异常)构造一个描述符,而是构造
//一个pollfd街头的数组,每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件
//最后一个参数timeout指定我们需要等待的时间和select一样
//1.timeout==-1 永远等待
//2.timeout==0  不等待
//3.timeout>0   等待timeout毫秒
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout)

//pollfd结构体如下
struct pollfd {
    int fd;
    short events;
    short revents;
};

//异步IO的结构体,具体实现可能会有更多的字段
struct aiocb {
    int     aio_filedes;
    off_t   aio_offset;
    volatile void *aio_buf;
    size_t  aio_nbytes;
    int     aio_reqprio;
    struct sigevent aio_sigevent;
    int     aio_lio_opcode;
};

//sigevent 结构体如下
struct sigevent {
    int   sigev_notify;
    int   sigev_signo;
    union sigval sigev_value;
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t *sigev_notify_attributes;
};

//sigev_notify字段控制通知的类型,取值可能是以下3个中的一个
//1.SIGEV_NONE    异步I/O请求完成后,不通知进程
//2.SIGEV_SIGNAL  异步I/O请求完成后,产生由sigev_signo字段指定的信号。如果应用程序已选择
//   捕捉信号,且在建立信号处理程序的时候指定了SA_SIGINFO标志,那么该信号将被入队列。信号
//   处理程序会传送一个siginfo结构。该结构中的si_value字段被设置为sigev_value
//3.SIGEV_THREAD  当异步I/O完成时,由sigev_notify_function字段指定的函数被调用。sigev_value
//字段被传入作为它的唯一参数。除非sigev_notify_attributes字段被设定为pthread属性结构的
//地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行

//异步IO读写函数
#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *atiocb);

//要想强制所有等待中的异步操作部等待而写入持久化存储中,可以调用下面函数
#include <aio.h>
int aio_fsync(int op, struct aiocb *aiocb);

//获知一个异步读,写或者同步操作的完成状态,返回值是下面4种之一
//0    异步操作成功完成,需要调用aio_return函数获取操作返回值
//-1   对aio_error调用失败,调用errno会告诉我们为什么
//EINPROGRESS  异步读,写或同步操作仍在等待
//其他情况   其他任何返回值是相关异步操作失败返回的错误码
#include <aio.h>
int aio_error(const struct aiocb *aiocb);

//如果异步操作成功,可以调用aio_return函数来获取操作的返回值
#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);

//执行IO操作时,如果还有其他事物要处理不想被IO操作阻塞,可以使用异步IO。如果在事务完成时,
//还有异步操作未完成,可以调用下面函数来阻塞进程直到操作完成
#include <aio.h>
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout)

//当有不想等待完成的异步IO操作时,调用下面函数取消,返回值是下面4种之一
//AIO_ALLDONE     所有操作在尝试取消他们之前已经完成
//AIO_CANCELED    所有要求的操作已被取消
//AIO_NOTCANCELED 至少有一个要求的操作没有被取消
//-1              对aio_cancel的调用失败,错误码存储在errno中
#inclue <aio.h>
int aio_cancel(int fd, struct aiocb *aiocb);

//还有一个函数被包含在异步IO中,它既能以同步方式使用,又能以异步的方式来使用
//mode参数决定了IO是否真的是异步的,如果参数是LIO_WAIT,函数将在所有由列表指定的IO操作完成
//后返回。这种情况下sigev参数将被忽略
//如果参数被设定为LIO_NOWAIT函数将在IO请求入队列后立即返回。按照sigev参数指定的被异步通知
#include <aio.h>
int lio_listio(int mode, struct aiocb *restrict const list[restrict], int nent,
                         struct sigevent *restrict sigev);

//聚集读(scatter read)和聚集写(gather write)
//write函数从缓冲区输出数据的顺序是iov[0],iov[1]直到iov[iovcnt-1]
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int invent);
ssize_t writev(int fd, const struct iovec *iov, int invent);

//iovec结构体
struct iovec {
    void *iov_base;   //缓冲区起始地址
    size_tiov_len;    //缓冲区大小
};


//存储映射函数
//addr参数指定映射存储区的起始地址,通常设置为0
//fd参数指定要被映射的文件描述符,prot参数至此那个了映射存储区的保护要求
//flag参数是存储映射区的属性
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);

//更改一个现有映射的全新,成功返回0,出错返回-1
#include <sys/mman.h>
int protect(void *addr, size_t len, int prot);

//如果共享映射中的页已修改,用下面函数刷新到文件中,类似fsync函数
#include <sys/mman.h>
int msync(void *addr, size_t len, int flag);

//下面函数可以解除映射区,关闭映射存储区时使用的文件描述符并不解除映射区
#include <mman.h>
int munmap(void *addr, size_t len);

 

 

 

对于一个给定的描述符,有两种方式指定为非阻塞I/O方式

1)如果调用open获得描述符,则可指定O_NONBLOCK标志

2)对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志

 

 

记录锁

记录锁(record locking)的功能是: 当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他

进程修改同一文件。“记录”这个词是一种误用,UNIX根本没有使用文件记录这种概念,更适合的术语是

字节范围锁(byte-range locking),因为它锁定的指示文件中的一个区域

 

fcntl 函数的3种命令

参数 说明
F_GETLK

判断flockptr所描述的锁是否会被另外一把锁排斥(阻塞)。如果存在一把锁,它阻止创建由

flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,

则除了将l_type 设置为FUNLCK之外,flockptr所指向结构中的其他信息保持不变

F_SETLK

设置由flockptr所描述的锁。如果我们试图获得一把读锁(l_type为F_RDLCK)或写锁(l_type或

F_WRLCK),而兼容性规则阻止系统给我们这把锁。那么fcntl 会立即出错返回,此时errno

设置为EACCES或EAGAIN

F_SETLKW

这个命令式F_SETLK的阻塞版本。如果所请求的读锁或写锁因另一个进程当前已经对所

请求区域的某部分进了加锁而不能被授权,那么调用进行会被设置为休眠。如果请求创建

的锁已经可用,或者休眠由信号中断,则该进程被唤醒

 

在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区域

若100--199字节加锁,释放150字节,则内核会维持两把锁。如果又对150字节加锁,最后又变成一把锁

 

关于记录锁的自动继承和释放有三条规则

1)锁与进程和文件两者相关联,当进程终止时它建立的锁全部释放;无论一个描述符何时关闭,该进程通过这

   一描述符引用的文件上的任何一把锁都会释放

2)由fock产生的子进程不继承父进程所设置的锁

3)执行exec后,新进程可以继承原执行程序的锁

 

关于记录锁的FreeBSD实现

UNIX环境高级编程 高级I/O_第1张图片
和之前给出的open,fork,dup的数据结构类似,新加了lockf结构。他们由i 节点结构开始相互链接起来。

每个lockf结构描述了一个给定的加锁区域 

 

建议性锁和强制性锁

强制性锁会让内核检查每一个open,read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。

强制性锁有时候也称为强迫方式锁(enforcement-mode locking) 

 

 

I/O多路转换

如果同时从两个描述符读,又同时写入两个描述符。此时可以用进程+信号量,线程+互斥量实现

或者用多路转换IO实现

传给select的参数告诉内核

1)我们所关心的描述符

2)对于每个描述符我们所关心的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符,是否关心

    一个给定描述符的异常条件)

3)愿意等待多长时间

从select返回时,内核告诉我们

1)已准备好的描述符的总数量

2)对于读,写或异常这三个条件中的每一个,那些描述符已准备好

 

poll 的events和revents标志

标志名 输入至events 从revents得到结果 说明
POLLIN 是 

可以不阻塞的读高优先级数据意外的数据(等效于

POLLRDNORM | POLLRDBAND

POLLRDNORM 可以不阻塞的读普通数据
POLLRDBAND 可以不阻塞的读优先级数据
POLLOUT 可以不足赛的写普通数据
POLLWRNORM 与POLLOUT相同
POLLWRBAND 可以不阻塞的写优先级数据
POLLERR   已出错
POLLHUP   已挂断
POLLNVAL   描述符没有引用一个打开文件

 

 

 

异步I/O

使用POSIX异步I/O接口会有以下麻烦

1)每个异步操作有3处可能产生错误的地方

   a)操作提交的部分

   b)操作本身的结构

   c)用于决定异步操作状态的函数中

2)与POSIX异步I/O的传统方法相比,他们本身涉及大量的额外设置和处理规则

3)从错误中恢复可能会比较困难,比如提交了多个异步写操作,其中一个失败怎么办。如果这些写操作是相关

   的,那么可能还需要撤销所有成功的写操作

 

在System V中,异步I/O是STREAMS系统的一部分,它只对STREAMS设备和STREAMS管道起作用

System V的异步信号是 SIGPOLL

产生SIGPOLL信号的条件

常量 说明
S_INPUT 可以不阻塞的读取数据(非高优先级数据)
S_RDNORM 可以不足赛的读取普通数据
S_RDBAND 可以不阻塞的读取优先级数据
S_BANDURG

若此常量和S_RDBAND一起指定,当我们可以不阻塞的读取优先数据时,

产生SIGURG信号而非SIGPOLL

S_HIPRI 可以不阻塞的读取高优先级数据
S_OUTPUT 可以不阻塞的写普通数据
S_WRNORM 与S_OUTPUT相同
S_WRBAND 可以不阻塞的写优先级数据
S_MSG 包含SIGPOLL信号的消息已经达到流头部
S_ERROR 流有错误
S_HANGUP 流已挂起

 

为了接收SIGIO信号,需要以下三部

1)调用signal或sigaction为SIGIO信号建立信号处理程序

2)以命令F_SETOWN调用fcntl来设置进程ID或进程组ID,用于接收对于该描述符的信号

3)以命令F_SETFL调用fcntl设置O_ASYNC文件状态标志,使在该描述符上可以进行异步I/O

 

散布读(scatter read)的示意图

UNIX环境高级编程 高级I/O_第2张图片
 

 

管道,FIFO以及某些设备(特别是终端和网络)有下列两种性质

1)一次read操作的返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样,这不是一个错误

   应该继续读该设备

2)一次write操作的返回值也可能少于指定输出的字节数。这可能是由某个因素造成的。如内核输出缓冲区变

   满。这也不是错误,应当继续写余下字节(通常只有费阻塞描述符或捕捉到一个信号时,才发生这种write

   中途返回)

我们可以实现自定义的readn 和 writen 函数

//指定fd后, 读取或者写入缓冲区,必须达到指定的字节后才返回
ssize_t readn(int fd, void *buf, size_t nbytes);
ssize_t writen(int fd, void *buf, size_t nbytes);

  

 

 

存储映射I/O(memory-mapped I/O)

可以将一个磁盘文件映射到存储空间中的一个缓冲区上,当从缓冲区读数据时,就相当于读文件中的相应字节。

当写入缓冲区时,相应的字节就自动写入文件,可以在不适用read和write函数的情况下执行I/O

 

映射存储区的保护要求

prot 说明
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问

 

映射函数flag参数影响存储区的多种属性(具体实现会有更多参数,参见 man mmap结果)

参数 说明
MAP_FIXED

返回值必须等于addr,因为这不利于可移植性,所以不鼓励使用此标志。如果未指定此

标志,而addr非0,则内核只把addr视为在何处设置映射区的一种建议,但是不保证会

使用所要求的地址。addr指定为0可获得最大可移植性

MAP_SHARED

这一标志描述了本进程对映射区进行的存储操作的配置。此标志指定存储操作修改映射

文件,也就是存储操作相当于对该文件write,必须指定本标志或下一个标志

(MAP_PRIVATE),但不能同时指定两者

MAP_PRIVATE

本标志说明,对映射区的存储操作导致创建该映射文件的一个私有副本,所有后来对该

映射区的引用都是引用该副本(此标志的一种用途是用于调试程序,它将程序文件的

正文部分映射至存储区,但允许用户修改其中的指令。任何修改只影响程序文件的

副本,而不影响原文件)

 

存储映射文件的例子

UNIX环境高级编程 高级I/O_第3张图片

 

off的值和addr的值(如果指定了MAP_FIXED)通常被要求是兄台那个虚拟存储页长的倍数。

如果不是页长的倍数呢?假设文件长12字节,系统页长512字节,则提供512字节的映射区。其中后500字节为

0,可以修改后面这500字节,但任何变动都不会再文件中反映出来。

 

 

 

 

参考

select函数

 

 

 

 

你可能感兴趣的:(unix)