14.1:引言

本章内容包括非阻塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll函数)、readv和writev函数以及存储映射I/O(mmap),这些都称为高级I/O。

14.2:非阻塞I/O

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

对于一个给定的描述符有两种方法对其指定非阻塞I/O:

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

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

实例 14-1:长的非阻塞write

14.3:记录锁

记录锁(record lock)的功能是:当一个进程正在修改或读文件的某以部分时,它可以阻止其他进程修改同以文件区。

1. fcntl记录锁

#include <fcntl.h>
int fcntl(int filedes, int cmd, .../* struct flock *flockpty */);
// 返回值:若成功则依赖于cmd,若失败则返回-1

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数是一个指向flock结构的指针:

struct flock
{
short l_type; // F_RDLCK、F_WRLCK、or F_UNLCK
off_t l_start; // offset in bytes, relative to l_whence
short l_whence; // SEEK_SET、SEEK_CUR、or SEEK_END
off_t l_len; // length, in bytes; 0 means lock to EOF
pid_t l_pid; // returned with F_GETLK
};

对flock结果说明如下:

所希望的锁类型:F_RDLCK共享读锁、F_WRLCK独占性写锁、F_UNLCK解锁一个区域

要加锁或解锁区域的起始偏移量。这由l_start和l_whence决定

区域的字节长度,由l_len表示。

具有能阻塞当前进程的锁,其持有进程的进程ID放在l_pid中(仅由F_GETLK返回)

关于加锁和解锁区域的说明还要注意下列几点:

l_start是相对偏移量,l_whence则决定了相对偏移量的起点。l_whence的可选值是SEEK_SET、SEEK_CUR、SEEK_END。

如若l_len是0,则表示锁的区域从其起点(由l_start和l_whence决定)直至最大可能偏移量为止,也就是不管文件添加多少数据,它们都在锁的范围之内。

为了锁整个文件,我们设置l_start和l_whence,使锁的起点位于文件开始处,并且说明长度(l_len)为0。(有多种方法可以指定文件开始处,但最常见的方法是设置l_start为0,l_whence为SEEK_SET。)

锁的兼容性

共享锁和独占锁的基本规则是:多个进程在一个给定的字节可以有一把共享的读锁,但是在给定的字节上,只能有一个进程有一把独占的写锁。进一步而言,如果在给定的字节上有一把或多把读锁,则不能在该字节上加写锁;同样,如果再给定的字节上有一个写锁,则不能在字节上加读锁。

上述规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间有一把锁,后来该进程又企图在该文件区间上加另外一把锁,则新锁将替换老锁。

加读锁时,该文件必须是读打开;加写锁时,该文件必须是写打开。

以下说明fcntl函数的三种命令:

F_GETLK:判断由flockptr所描述的锁是否被另一把锁所排斥。如果一把排斥flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在一把排斥的锁,则只将l_type设置为F_UNLCK,flockptr指向的结构的其他信息不变。该函数的作用就是测试由flockptr所指向的锁能不能加到执行文件区间。

F_SETLK:设置由flockptr描述的锁。如果要建立一把读锁或写锁,按上述兼容性规则,如果不能创建该锁,则fcntl出错返回,此时errno设置为EAGAIN。此命令也用于清除锁,把l_type设置为F_UNLCK。

F_SETLKW:这是F_SETLK的阻塞版本(W表示wait)。如果当前所请求的区间的某一部分,另一个进程已有一把锁,而按兼容性规则由flockptr描述的锁无法创建,则进程休眠。如果请求的锁已可用,或者休眠由信号中断,则该进程被唤醒。

注意:

用F_GETLK测试一把锁,然后用F_SETLK或者F_SETLKW来设置锁,这两步不是一个原子操作。因此不能保证在使用F_SETLK时,测试的结果依然不变。

2. 锁的隐含继承和释放

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

(1)锁与进程和文件两方面有关。这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显了,任何时候关闭一个描述符时,则进程通过这一描述符可以引用的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。

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

(3)在执行exec后,新程序可以继承原执行程序的锁。但是请注意,如果对一个文件描述符设置了close-on-exec标志,那么当作为exec的一部分关闭该文件描述符时,对相应文件的所有锁都被释放了。

3.实例 在文件整体上加锁

我们了解到,守护进程可以利用一把锁来保证只有守护进程的唯一副本在运行。下面函数实现了这种机制。

#include <unistd.h>
#include <fcntl.h> int lockfile(int fd)
{
struct flock fl; fl.l_type = F_WRLCK;
fl.l_start = ;
fl.l_whence = SEEK_SET;
fl.l_len = ;
return fcntl(fd, F_SETLK, &fl);
}

还有另一种方法,是使用write_lock来实现:

#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)

4.在文件尾端加锁

在接近文件尾端加锁或解锁一定要小心。

如下代码:

write_lock(fd, , SEEK_END, );
write(fd, buf, );
un_lock(fd, , SEEK_END);
write(fd, buf, );

该代码所做的可能不是你所期望的。首先,它得到一把写锁,它从当前文件尾端起,包括以后可能添加到文件中的所有字节。然后,它在文件尾端添加了一个字节,该字节将被加锁。随后,解锁,但是刚才增加的那个字节将仍旧被锁着。最后,第二个写,这次写入的一个字节是不被加锁的。

所以,这段代码执行完之后,该文件中倒数第二个字节是被锁着的。跟我们的预期差别很远吧。

5.建议性锁和强制性锁

14.4:STREAMS

不太理解STREAMS机制、STREAMS设备是什么意思。

14.5:I/O多路转接

当从一个描述符读,然后又写入到另外一个描述符时,可以在下列形式的循环中使用阻塞I/O:

while ((n = read(STDIN_FILENO, buf, BUFSIZE)) > )
{
if (write(STDOUT_FILENO, buf, n) != n)
{
perror("write error!");
}
}

这种形式的阻塞I/O到处可见。但是如果必须从两个描述符读,又将如何呢?如果仍旧使用阻塞I/O,那么就可能长时间阻塞在一个描述符上,而另一个描述符虽有很多数据却不能得到及时处理。所以为了处理这种情况显然需要另一种不同的技术。

处理这种问题的一个方法是PPC或者TPC。即一个连接一个进程、一个连接一个线程,但是这样做也会增加进程间通信和线程间同步的复杂度。

还有一种方法是使用非阻塞I/O(nonblocking I/O)

基本方法是将两个输入描述符设置为非阻塞的,对第一个描述符发出read操作,如果有数据则处理,如果没有数据,则read立即返回。然后对第二个描述符做同样的操作。在此之后等待若干秒,循环上述操作。这种方式成为轮询。

这种方式的不足之处是浪费CPU。

还有一种技术称之为异步I/O(asynchronous I/O)

其基本思想是进程告诉内核,当一个描述符已准备好可以进行I/O时,用一个信号通知它。这种技术有两个问题。第一,并非所有系统都支持。其次,这种信号对每个进程而言只有一个,在接到该信号时进程无法判断是哪一个描述符已准备好可以进行I/O。为了确定是哪一个,仍需将这两个描述符都设置为非阻塞的,并顺序试执行I/O。

一种比较好的技术是使用I/O多路转接(I/O multiplexing)

先构造一张有关描述符的列表,然后调用一个函数,直到该描述符列表中的一个已准备好I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O。

poll、select、pselect这三个函数可以让我们实现I/O多路转接。

14.5.1:select、pselect函数

在所有依从POSIX的平台上,select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:

我们所关心的描述符。

对每个描述符,我们所关心的状态。(是否读一个给定描述符,是否写一个给定描述符,是否关心一个描述符的异常状态)

愿意等待多久。

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

已准备好I/O的描述符个数。

对于读、写、异常这三个状态中的每一个,哪些描述符已准备好。

使用这些信息就可以调用相应的I/O函数,并且确定这些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);
// 返回值:准备就绪的描述符数,若超时返回0,若出错返回-1

先说明最后一个参数,它说明愿意等待的时间:

struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
};

有三种情况:

tvptr=NULL 永远等待。

tvptr->tv_sec==0 && tvptr->tv_usec=0 完全不等待。

tvptr->tv_sec!=0 || tvptr->tv_usec != 0 等待指定时间。若超时,则返回0。

POSIX允许实现中修改timeval的值,所以在select返回后,你不能指望该结构保持之前的值。在Linux 2.4.22中,若在该指定时间尚未超时就返回,那么就将用余下的时间值更新该结构。注意与poll函数中对应参数做对比。

中间三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。

对fd_set类型可以进行的处理是:分配一个这种类型的变量;将这种类型的一个变量赋值给同类型的另一个变量;或对于这种类型的变量使用下列四个函数中的一个:

#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset); // 返回值:若fd在描述符集中则返回非0,否则返回0
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_sete *fdset);

调用FD_ZERO将一个指定fd_set变量的所有位置为0;调用FD_SET设置一个fd_set变量的指定位;调用FD_CLR清除一个fd_set变量的指定位;然后调用FD_ISSET测试fd_set变量的指定位是否设置。

select函数的中间三个参数的任意一个或全部都可以为NULL。如果三个都是NULL,则select提供一个高精度的计时器。

select函数的第一位参数maxfdp1的意思是“最大描述符加1”。

select有三个可能的返回值:

返回-1表示出错。

返回0表示没有描述符准备好。

返回正值表示已经准备好的描述符数。该值是三个描述符集中已准备好的描述符的和。

14.5.2:poll函数

poll函数类似于select,但其程序员接口不同。

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
// 返回值:准备就绪的描述符数,若超时返回0,若出错返回-1

与select不同,poll不是为每个状态构造一个描述符集,而是构造一个poll结构数组,每个数组元素指定一个描述符编号及其所关心的状态。

struct pollfd
{
int fd; // file descriptor to check
short events; // events of interest on fd
short revents; // events that occurred on fd
};

fdarray的个数由nfds参数指定。

应将events成员设置成以下值。通过这些值,告诉内核对该描述符我们关心哪些状态。返回时,内核设置revents成员,以说明对于该描述符已发生了什么事件。(注意,poll没有更改events成员,这与select不同,select修改其参数以指示哪一个描述符已准备好了。)

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

标志名    输入致events    从reevents得到结果    说明

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

POLLIN    *          *            不阻塞的可读高优先级外的数据

POLLRDNORM  *          *            不阻塞的可读普通数据

POLLRDBAND *          *            不阻塞的可读非0优先级波段数据

POLLPRI     *          *            不阻塞的可读高优先级数据

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

POLLOUT   *          *            不阻塞的可写普通数据

POLLWRNORM *            *            与POLLOUT相同

POLLWRBAND *           *            不阻塞的可写非0优先级波段数据

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

POLLERR               *            已出错

POLLHUP              *            已挂断

POLLNVAL              *            描述符不引用一个打开文件

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

前四行测试可读性,中间三行测试可写性,最后三行测试异常状态。最后三行是由内核在返回时设置的,即使在events中没有设置这三个值,如果有异常情况发生,也会在reevents中返回它们。

poll的最后一个参数表示我们愿意等待多长时间。与select类似,有三种情况:

timeout == -1 永远等待。捕捉信号返回,则poll返回-1,errno设置为EINTR。

timeout == 0 不等待。

timeout > 0 等待timeout毫秒。超时返回0

应当理解文件结束和挂断的区别。如果正从终端输入数据,并键入文件结束符,POLLIN被打开,于是就可读文件结束指示(read 0)。POLLHUP在revents中没有打开。如果正读调制解调器,电话线已挂断,则在revents中将接到POLLHUP通知。

与select一样,不论描述符是否阻塞,都不影响poll是否阻塞。

select与poll的可中断性

在接到信号后,select和poll都不自动重启。

14.6:异步I/O

使用上一节的select、poll可以实现异步形式的通知。关于描述符的状态,系统并不主动告诉我们,需要我们主动查询(调用select或poll)。信号机构提供一种以异步形式通知某种事件已发生的方法。

但是异步I/O的一个限制是每个进程只有一个信号。如果要对几个描述符进行异步I/O,那么在进程收到该信号时并不知道信号对应于哪一个描述符。

14.7:readv和writev函数

readv和writev函数用于在一次函数调用中读取、写入多个缓冲区。也称为散布读、聚集写。

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
// 两个函数返回值,若成功,返回已读、写的字节数,若失败返回-1

这两个函数的第二个参数是指向iovec结构数组的指针:

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

14.8:readn和writen函数

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

(1)一次read返回的数据可能少于要求的数据。

(2)一次write返回的个数也可能小于要写入的数据长度。

readn、writen的功能是读写指定的N字节数据,并处理返回值小于要求值的情况。这两个函数只是按需多次调用了read、write直至读写了N字节数据。

#include "apue.h"
ssize_t readn(int filedes, void *buf, size_t nbytes);
ssize_t writen(int filedes, void *buf, size_t nbytes);
// 两个函数返回值:已读写字节数,若出错返回-1

注意:这两个函数并非任何标准,而是apue这本书中作者写出来的,方便以后使用。

14.9:存储映射I/O

存储映射I/O使一个磁盘文件和存储空间中的一个缓冲区相映射,于是当从缓冲区中读取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动写入文件。

为了使用这种功能,应首先告诉内核将一个给定文件映射到一个存储区中。这是由mmap函数实现的:

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

addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由操作系统选择该映射区的起始地址。此函数的返回地址是该映射区的起始地址。

filedes指定要被映射问价的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。off是要映射字节在文件中的起始偏移量。

prot参数说明对映射存储区的保护要求。

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

flag参数影响映射存储区的多种属性

MAP_FIXED 返回值必须等于addr。因为这不利于可移植性,所以不建议使用此标志。如果未指定此标志,而addr非0,则内核只把addr视为一种建议,但是不保证会使用该起始地址。
MAP_SHARED 这一标志说明了本进程对映射存储区所进行的存储操作配置。此标志指定存储操作修改映射文件,也就是说,存储操作相当于对该文件的write操作。必须指定本标志或下一个标志,但不能同时指定。
MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。

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

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

如果在共享映射存储区中的页已被修改,那么我们可以调用msync将该页冲洗到被映射的文件中。msync函数类似与fsync,但作用于共享存储区。

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

如果映射是私有的,那么不修改被映射的文件。flags参数使我们对如何冲洗存储区有某种程度的控制。我们可以指定MS_ASYNC标志以简化被写页的调度。如果我们希望在返回之前等待写操作完成,则可以指定MS_SYNC标志。一定要指定MS_ASYNC和MS_SYNC中的一个。

进程终止时,或调用了munmap函数之后,存储映射区就被自动解除映射。关闭文件描述符filedes并不解除映射区。

#include <sys/mman.h>
int munmap(caddr_t addr, size_t len); // 返回值:若成功则返回0,若出错则返回-1

munmap不会影响被映射对象,调用munmap不会将映射存储区的内容写到磁盘文件上。对于MAP_SHARED区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。

在解除了映射后,对于MAP_PRIVATE存储区的修改被丢弃。

实例 14-12:用存储映射I/O复制一个文件。

14.10:小结

本章说明了很多高级I/O功能。

非阻塞I/O--发一个I/O操作,不使其阻塞。

记录锁

系统V流机制

I/O多路转接--select、poll函数

readv和writev函数

存储映射I/O(mmap)

第十四章:高级I/O的更多相关文章

  1. UNP学习笔记(第十四章 高级I/O函数)

    本章讨论我们笼统地归为“高级I/O”的各个函数和技术 套接字超时 有3种方法在涉及套接字的I/O操作上设置超时 1.调用alarm,它在指定超时时期满时产生SIGALRM信号 2.在select中阻塞 ...

  2. apue学习笔记(第十四章 高级I/O)

    本章涵盖了从多概念和函数:非阻塞I/O.记录锁.I/O多路转换.异步I/O.readv和writev函数以及存储映射I/O 非阻塞I/O 非阻塞I/O使我们可以发出open.read和write这样的 ...

  3. JavaScript高级程序设计:第十四章

    第十四章 一.表单的基础知识 在HTML中,表单是由<form>元素来表示的,而在javascript中,表单对应的则是HTMLFormElement类型.HTMLFormElement继 ...

  4. Python 3标准库 第十四章 应用构建模块

    Python 3标准库 The Python3 Standard Library by  Example -----------------------------------------第十四章   ...

  5. 《Linux命令行与shell脚本编程大全》 第十四章 学习笔记

    第十四章:呈现数据 理解输入与输出 标准文件描述符 文件描述符 缩写 描述 0 STDIN 标准输入 1 STDOUT 标准输出 2 STDERR 标准错误 1.STDIN 代表标准输入.对于终端界面 ...

  6. perl 第十四章 Perl5的包和模块

    第十四章 Perl5的包和模块 by flamephoenix 一.require函数  1.require函数和子程序库  2.用require指定Perl版本二.包  1.包的定义  2.在包间切 ...

  7. Gradle 1.12 翻译——第十四章. 教程 - 杂七杂八

    有关其它已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或訪问:http://gradledoc.qiniudn.com ...

  8. C和指针 (pointers on C)——第十四章:预处理器

    第十四章 预处理器 我跳过了先进的指针主题的章节. 太多的技巧,太学科不适合今天的我.但我真的读,读懂.假设谁读了私下能够交流一下.有的小技巧还是非常有意思. 预处理器这一章的内容.大家肯定都用过.什 ...

  9. CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章

    第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...

  10. C#语言和SQL Server第十三 十四章笔记

    十三章  使用ADO.NET访问数据库 十四章使用ADO.NET查询和操作数据库 十三章:                                                       ...

随机推荐

  1. JAVA-封装-静态属性

    1.使用 1.static 2.用来修饰属性.方法.内部类.代码块 3.称为类属性,静态属性,类方法,静态方法 3.不需要实例化,直接用类名或静态成员名调用 2.特点 1.静态属性对于类的所有实例是共 ...

  2. !! 浅谈Java学习方法和后期面试技巧

    浅谈Java学习方法和后期面试技巧 昨天查看3303回复33 部落用户大酋长 下面简单列举一下大家学习java的一个系统知识点的一些介绍 一.java基础部分:java基础的时候,有些知识点是非常重要 ...

  3. 树形dp Anniversary party(HDU1520)

    题意:给出一棵树,(上下级关系)每个节点都有一个权值,要求选出一些节点满足这些节点任意连个点都不是直接的上下级关系,可以得到的最大权值是多少? 分析:对于每个点有两个状态选或者不选,用状态数组dp[u ...

  4. Java基础(36):String与基本数据类型之间的双向转换(Wrapper类)

    Java 中基本类型和字符串之间的转换 在程序开发中,我们经常需要在基本数据类型和字符串之间进行转换. 其中,基本类型转换为字符串有三种方法: 1. 使用包装类的 toString() 方法 2. 使 ...

  5. Android -- 自定义View小Demo(一)

    1,现在要实现下图的简单效果,很简单  ,就是使用paint在canvas上绘制5中不同颜色的圆圈,效果图如下: 这是绘制基本图形一种最简单的方法,下面是它的代码 ,注释写的很详细,也就不去讲解了 M ...

  6. oracle的系统文件的查询

    1:查看实例和数据库的相关信息 --查看实例 select instance_name,version,status,archiver,database_status from v$instance; ...

  7. EBS R12版 GL追溯到各个模块

    应收.应付.收款.付款等单据都可以生成ERP的日记帐,那么这些模块的关系是如何关联的呢,我们将会解决这个问题. 各个模块与总帐模块的关系,主要是通过子分类帐来进行关联的. 下面的SQL就是总帐与子分类 ...

  8. 夺命雷公狗---DEDECMS----18dedecms之无可奈何标签-sql标签取出今天更新

    我们在一些开发时候遇到普通标签都解决不了的问题的时候可以尝试下我们dedecms自带的sql标签,几乎可以完成任何的查询需求 语法如下所示: 我们在这里将刚才首页今天更新那块给改写下,原先的是: {d ...

  9. 帮初学者改代码——有多少青春可以挥霍之“c语言 多重排序”

    原文:“c语言 多重排序” 原代码: #include<stdio.h> #include<string.h> struct A { char name[100]; int g ...

  10. uploadify3.2.1加载时,报NetworkError 404 Not Found或NetworkError forbidden错误

    我用的uploadify的版本是3.2.1 在打开配置了uploadify的页面的时候,什么操作都没有,仅仅是打开了页面,在火狐里可以看到一行报错信息,我的uploadify页面 在"/项目 ...