《UNIX环境高级编程》十四高级I/O读书笔记

1、非阻塞I/O(同步)

系统调用分成两类:“低速”系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用。

非阻塞I/O使我们可以发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。

这里写图片描述

这里写图片描述

对于一个给定的文件描述符,有两种为其指定非阻塞I/O的方法:
(1)如果调用open获得描述符,则可指定O_NONBLOCK标志。
(2)对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

对于一个文件描述符设置一个或多个文件状态标志:

#include "apue.h"
#include <fcntl.h>
void set_f1(int fd,int flags)
{
    int val;
    if((val=fcntl(fd,F_GETFL,0))<0)
        err_sys("fcntl F_GETFL error");
    val |=flags;
    if(fcntl(fd,F_SETFL,val)<0)
        err_sys("fcntl F_SETFL error");
}

2、记录锁

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

fcntl记录锁

#include <fantl.h>
int fcntl(int fd,int cmd,.../*struct flock *flockptr */);
若成功,依赖于cmd,否则,返回-1.

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。
F_GETLK:判断由flockptr所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
F_SETLK:设置由flockptr所描述的锁。如果我们试图获得一把读锁(l_type为F_RDLCK)或写锁(l_type为F_WRLCK),而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回。此时errno设置为EACCES或EAGAIN。此命令也用来清除由flockptr指定的锁(l_type为F_UNLCK)。
**F_SETLKW:**F_SETLK的阻塞版本。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会被置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。

第三个参数是一个指向flock结构的指针:

struct flock{
short l_type; //F_RDLCK,F_WRLCK或F_UNLCK
short l_whence;//SEEK_SET,SEEK_CUR或SEEK_END
off_t l_start;//起始字节偏移量
off_t l_len;//区域的字节长度
pid_t l_pid;进程ID持有的锁能阻塞当前进程
};

注:
如若l_len为0,则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向该文件中追加写了多少数据,它们都可以处于锁的范围内。
为了对整个文件加锁,设置l_start和l_whence指向文件的起始位置,并且指定长度(l_len)为0.

  • 对于不同进程提出的锁请求:任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程有一把独占写锁。
  • 对于单个进程提出的多个锁请求:如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。

加读锁时,该描述符必须是读打开。加写锁是,该描述符必须是写打开。

锁的隐含继承和释放

关于记录锁的自动继承和释放有3条规则:
(1)锁与进程和文件两者相关联。有两重含义:一是当一个进程终止时,它所建立的锁全部释放;二是无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放。
ex:
fd1=open(pathname,…);
read_lock(fd1,…);
fd2=dup(fd1);//或fd2=open(pathname,…);
close(fd2);
在close(fd2)后,在fd1上设置的锁被释放。如果将dup替换为open,其效果也一样。
(2)由fork产生的子进程不继承父进程所设置的锁。
(3)在执行exec后,新程序可以继承原执行程序的锁。但注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁。

FreeBSD实现

这里写图片描述
这里写图片描述
在父进程中,关闭fd1、fd2或fd3中的任意一个都将释放由父进程设置的写锁。

建议性锁和强制性锁

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

对一个特定文件打开其设置组ID位、关闭其组执行位便开启了对该文件的强制性锁机制。
这里写图片描述

3、I/O多路转接(异步阻塞I/O)

这里写图片描述
telnet进程有两个输入、两个输出。我们不能对两个输入中的任一个使用阻塞read,因为我们不知道到底哪一个输入会得到数据。
处理这种特殊问题的一种方法是,将一个进程变成两个进程(用fork),每个进程处理一条数据道路。如果使用两个进程,则可使每个进程都执行阻塞read。这里写图片描述

但一种比较好的技术是使用I/O多路转接。为了使用这种技术,先构造一张我们感兴趣的描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数才返回。poll、pselect和select这3个函数使我们能够执行I/O多路转接。在从这些函数返回时,进程会被告知哪些描述符已准备好可以进行I/O。
这里写图片描述

3.1、函数select和pselect

从select返回时,内核告诉我们:
- 已准备好的描述符的总数量;
- 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好。
使用这种返回信息,就可调用相应的I/O函数(read或write),并且确知该函数不会阻塞。

#include <sys/select.h>
int select(int maxfdap1,fd_set *restrict readfds,
           fd_set *restrict writefds,fd_set *restrict exceptfds,
           struct timeval *restrict tvptr);
返回准备就绪的描述符数目;若超时,返回0;若出错,返回-1

maxfdp1参数意思是“最大文件描述符编号加1”,即最大描述符数。
tvptr指定愿意等待的时间长度,单位为秒和微妙:
tvptr == NULL:
永远等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR。
tvptr->tv_sec == 0 && tvptr->tv_usec ==0:
根本不等待。测试所有指定的描述符并立即返回。
tvptr->tv_sec != 0 && tvptr->tv_usec !=0:
等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。

这里写图片描述
每个描述符集存储在一个fd_set数据类型中。对于fd_set数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用下列4个函数中的一个:

#include <sys/select.h>
int FD_ISSET(int fd,fd_set *fdset);//从select返回时,可以用FD_ISSET测试该集中的一个给定位是否仍处于打开状态。
若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);//将fdset变量的所有位设置为0

注:在声明了一个描述符集之后,必须用FD_ZERO将这个描述符集置为0,然后在其中设置我们关心的各个描述符的位。

select有3个可能的返回值:
(1)返回值-1表示出错。例如,在所指定的描述符一个都没准备好时捕捉到一个信号。这种情况下,一个描述符集都不修改。
(2)返回值0表示没有描述符准备好。若指定的描述符一个都没准备好,指定的时间就过了,那么就会发生这种情况。此时,所有描述符集都会置0。
(3)一个正返回值说明了已经准备好的描述符数。该值是3个描述符集中以准备好的描述符数之和。在这种情况下,3个描述符集中仍旧打开的位对应于已准备好的描述符。
注:对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

#include <sys/select.h>
int pselect(int maxfdap1,fd_set *restrict readfds,
           fd_set *restrict writefds,fd_set *restrict exceptfds,
           const struct timespec *restrict tvptr,
           const sigset_t *restrict sigmask);
//返回准备就绪的描述符数目;若超时,返回0;若出错,返回-1

3.2、函数poll

#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
//返回准备就绪的描述符数目;若超时,返回0;若出错,返回-1
struct pollfd{
int fd;         /*file descriptor to check,or<0 to ignore*/ 
short events;   /*events of interest on fd*/
short revents;  /*enents that occurred on fd*/
};

nfds:数组中的元素数。

events成员:
这里写图片描述

timeout指定愿意等待多长时间:
timeout == -1:永远等待。
timeout == 0:不等待。
timeout>0:等待timeout毫秒。

4、异步I/O(非阻塞)

这里写图片描述
使用上一节说明的select和poll可以实现异步形式的通知。
信号SIGIO通知进程,对某个描述符所关心的某个事件已经发生。

异步I/O接口使用AIO控制块来描述I/O操作。AIO背后的基本思想是允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成。稍后或在接收到I/O操作完成的通知时,进程就可以检索I/O操作的结果。

struct aiocb{
int               aio_fildes;/*file decsriptor*/
off_t             aio_offset;/*file offset forI/O*/
volatile void    *aio_buf;   /*buffer for I/O*/
size_t            aio_nbytes;/*number of bytes to transfer */
int               aio_reqprio;/*priority*/
struct sigevent   aio_sigevent;/*signal information*/
int               aio_lio_opcode;/*operation for list I/O*/
};

异步I/O操作必须显示地指定偏移量,但如果使用异步I/O接口向一个以追加模式(使用O_APPEND)打开的文件中写入数据,aio_offset字段会被系统忽略。

aio_sigevent字段控制,在I/O事件完成后,如何通知应用程序:

struct sigevent {
int          sigev_notify; /*notify type */
int          sigev_signo;  /*signal number*/
union sigval sigev_value;  /*notify argument*/
void (*sigev_notify_function)(union sigval); /*notify function*/
pthread_attr_t *sigev_notify_attributes; /*notify attrs*/
};

sigev_notify字段控制通知的类型:
SIGEV_NONE:异步I/O请求完成后,不通知进程。
SIGEV_SIGNAL:异步I/O请求完成后,产生由sigev_signo字段指定的信号。
SIGEV_THREAED:当异步I/O请求完成后,由sigev_notify_function字段指定的函数被调用。sigev_value字段被传入作为它的唯一参数。

在进行异步I/O之前需要先初始化AIO控制块,调用aio_read函数来进行异步读操作,或调用aio_write函数来进行异步写操作:

#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
//若成功,返回0;若出错,返回-1

当这些函数返回成功时,异步I/O请求便已经被操作系统放入等待处理的队列中了。

要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并调用aio_fsync函数:

#include <aio.h>
int aio_fsynv(int op,struct aio *aiocb);
//若成功,返回0;若出错,返回-1

op参数:
O_DSYNC:类似于调用fdatasync。
O_SYNC:类似于调用fsync。

为了获知一个异步读、写或者同步操作的完成状态,需要调用aio_error函数:

#include <aio.h>
int aio_error(const struct aiocb *aiocb);

返回值为下面4中情况:
0:异步操作成功完成。需要调用aio_return函数获取操作返回值。
-1:对aio_error的调用失败。
EINPROGRESS:异步读、写或同步操作仍在等待。
其他情况:其他任何返回值是相关的异步操作失败返回的错误码。

如果异步操作成功,可以调用aio_return函数来获取异步操作的返回值:

#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);
//如果aio_return函数本身失败,会返回-1,并设置error。其他情况下,它将返回异步操作的结果,即会返回read、write或者fsync在被成功调用时可能返回的结果。

注:直到异步操作完成之前,都需要小心不要调用aio_return函数。操作完成之前的结果是未定义的。还需要小心对每个异步操作只调用一次aio_return,一旦调用了该函数,操作系统就可以释放掉包含了I/O操作返回值的记录。

如果在完成了所有事务时,还有异步操作未完成时,可以调用aio_suspend函数来阻塞进程,知道操作完成:

#include <aio.h>
int aio_suspend(const struct aiocb *const list[],int nent,
                const struct timespec *timeout);
//若成功,返回0;若出错,返回-1

如果被一个信号中断,aio_suspend将会返回-1.并将errno设置为EINTR。如果在没有任何I/O操作完成的情况下,阻塞的时间超过了函数中可选的timeout参数所指定的时间限制,返回-1,并将error设置为EAGAIN。如果有任何/O操作完成,返回0.如果在调用aio_suspend操作时,所有的异步I/O操作都已完成,那么aio_suspend将在不阻塞的情况下直接返回。

当还有我们不想再完成的等待中的异步I/O操作时,可以尝试使用aio_cancel函数来取消它们:

#include <aio.h>
int aio_cancel(int fd,struct aiocb *aiocb);

fd参数指定了那个未完成的异步I/O操作的文件描述符。如果aiocb参数为NULL,系统将会尝试取消所有该文件上未完成的异步I/O操作。
aio_cancel函数可能返回以下4个值中的一个:
AIO_ALLDONE:所有操作在尝试取消它们之前已经完成。
AIO_CANCELED:所有要求的操作已被取消。
AIO_NOTCANCELED:至少有一个要求的操作没有被取消。
-1:对aio_cancel的调用失败,错误码将被存储在errno中。

lio_listio函数提交一系列由一个AIO控制块列表描述的I/O请求:

#include <aio.h>
int lio_listio(int mode,struct aiocb *restrict const list[restrict],int nent,struct sigevent *restrict sigev);
//若成功,返回0;若出错,返回-1

mode参数决定了I/O是否异步。
如果该参数被设定为LIO_WAIT,lio_listio函数将在所有由列表指定的I/O操作完成后返回。在这种情况下,sigev参数将被忽略。
如果mode参数被设定为LIO_NOWAIT,lio_listio函数将在I/O请求入列后立即返回。进程将在所有I/O操作完成后,按照sigev参数指定的,被异步地通知。

在每一个AIO控制块中,aio_lio_opcode字段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是将被忽略的操作(LIO_NOP)。读操作会按照对应的AIO控制块被传给了aio_read函数来处理。类似地,写操作会按照对应的AIO控制块被传给aio_write函数来处理。

5、函数readv和writev

readv(散布读)和writev(聚集写)函数用于在一次函数调用中读、写多个非连续缓冲区。

#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

iovec数组中的元素数由iovcnt指定。

struct iovec{
void *iov_base;  /*starting address of buff*/
size_t iov_len;  /*size of buffer*/
};

这里写图片描述

6、存储映射I/O

存储映射I/O能将一个磁盘文件映射到存储空间中的一个缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区中,相应字节就自动写入文件。这样,就可以在不使用read和write的情况下执行I/O

告诉内核将一个给定的文件映射到一个存储区域中:

#include <sys/mman.h>
void *mmap(void *addr,size_t len,int prot,int falg,int fd,off_t off);
//若成功,返回映射区的起始地址,若出错,返回MAP_FAILED

addr参数用于指定映射存储区的起始地址,通常将其设置为0,表示由系统选择该映射区的起始地址。
fd参数是指定要被映射文件的描述符,在文件映射到地址空间之前,必须先打开该文件。len参数是映射的字节数,off是要映射字节在文件中的起始偏移量。
prot参数指定了映射存储区的保护要求:
这里写图片描述
对指定映射存储区的保护要求不能超过文件open模式访问权限。

flag参数:
这里写图片描述
这里写图片描述

这里写图片描述

与映射区相关的信号由SIGSEGV和SIGBUS。信号SIGEGV通常用于指示进程试图访问对它不可用的存储区。如果映射区的某个部分在访问时已不存在,则产生SIGBUS信号。

子进程能通过fork继承存储映射区。

调用mprotect可以更改一个现有映射的权限:

mprotect(void *addr,size_t len,int prot);
//若成功,返回0;若出错,返回-1

如果共享映射中的页已修改,那么可以调用msync将该页冲洗到被映射的文件中:

#include <sys/mman.h>
int msync(void *addr,size_t len,int flags);
//若成功,返回0;若出错,返回-1

这里的flags参数使我们对如何冲洗存储区有某种程度的控制:可以指定MS_ASYNC标志来简单地调试要写的页。如果希望在返回之前等待写操作完成,则可指定MS_SYNC标志。MS_INVALIDATE是一个可选标志,允许我们通知操作系统丢弃那些与底层存储器没有同步的页。

当进程终止时,会自动解除存储映射区的映射,或者直接调用munmap函数也可以解除映射区:

#include <sys/mman.h>
int munmap(void *addr,size_t len);
//若成功,返回0;若出错,返回-1
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值