UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)

低速系统调用为可能使进程永远阻塞的一类系统调用:
1.如果某些文件类型(管道、终端设备、网络设备)的数据不存在,读操作可能会使调用者永远阻塞。
2.如果数据不能被某文件类型立即接受(管道中无空间,网络流控制),写操作可能会使调用者永远阻塞。
3.再某条件发生前打开某文件类型可能发生阻塞(如要打开一个终端设备,需要先等待与之连接的调制解调器应答;如以只写模式打开FIFO,在没有其他进程已用读模式打开该FIFO时也要等待)。
4.对已经加上强制性记录锁的文件进行读写。
5.某些ioctl操作。
6.某些进程间通信函数。

虽然读写磁盘文件会暂时阻塞调用者,但不将与磁盘IO有关的系统调用视为低速。

非阻塞IO当我们发出open、read、write等IO操作时,使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。

将给定描述符指定为非阻塞IO:
1.如调用open获得描述符,可指定O_NONBLOCK标志。
2.对于已经打开的描述符,可用fcntl函数打开O_NONBLOCK文件状态标志。

POSIX.1要求非阻塞的描述符如无数据可读,read返回-1,errno设置为EAGAIN。

System V的早期版本使用标志O_NDELAY指定非阻塞方式,此时,如果无数据可读,read返回0,而UNIX又常将read的返回值0解释为文件结束,两者有所混淆。现在对O_NDELAY的支持是为了向后兼容,新程序不应使用它。

4.3BSD为fcntl提供了FNDELAY标志,它不仅将描述符的文件状态标志改成非阻塞方式,同时将终端设备或套接字的标志更改成非阻塞的,因此它不仅影响共享同一文件表项的用户,而且对终端或套接字的所有用户起作用(4.3BSD非阻塞IO只对终端和套接字起作用)。此时如果一个非阻塞描述符不能无阻塞地完成,会返回EWOULDBLOCK。现今基于BSD的系统提供POSIX.1的O_NONBLOCK代替FNDELAY,且将EWOULDBLOCK定义为与EAGAIN相同,这些基于BSD的系统提供与POSIX一致的非阻塞语义:即文件状态标志改变只影响同一文件表项的所有用户,与通过其他文件表项访问同一设备的用户无关。

下面是非阻塞IO的实例,它从标准输入读500000字节,并试图将其写到标准输出:

#include 
#include 
#include 
#include 
#include 

char buf[500000];

void set_fl(int fd, int flags) {
    int val;
    
    if ((val = fcntl(fd, F_GETFL, 0)) < 0) {
        printf("fcntl F_GETFL error\n");
        exit(1);
    }
     
    val |= flags;    // 打开flags
     
    if (fcntl(fd, F_SETFL, val) < 0) {
        printf("fcntl F_SETFL error\n");
        exit(1);
    }
}

void clr_fl(int fd, int flags) {
    int val;

    if ((val = fcntl(fd, F_GETFL, 0)) < 0) {
        printf("fcntl F_GETFL error\n");
		exit(1);
    }

    val &= ~(flags);

    if (fcntl(fd, F_SETFL, val) < 0) {
        printf("fcntl F_SETFL error\n");
		exit(1);
    }
}

int main() {
    int ntowrite, nwrite;
    char *ptr;

    ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
    fprintf(stderr, "read %d bytes\n", ntowrite);

    set_fl(STDOUT_FILENO, O_NONBLOCK);
    
    ptr = buf;
    while (ntowrite > 0) {
        errno = 0;
		nwrite = write(STDOUT_FILENO, ptr, ntowrite);
		fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);
	
		if (nwrite > 0) {
		    ptr += nwrite;
		    ntowrite -= nwrite;
		}
    }
    clr_fl(STDOUT_FILENO, O_NONBLOCK);
        
    exit(0);
}

如果标准输出是普通文件,则write应该只执行一次:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第1张图片
如果标准输出是终端,则write有时返回小于500000的一个数字,有时返回错误:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第2张图片
在该系统上,errno值35对应EAGAIN(在非阻塞模式下调用阻塞操作,没有完成就返回时会产生此错误)。终端驱动程序一次能接受的数据量随系统而变。

如果在终端上运行一个窗口系统,也是经由伪终端设备与系统交互。

以上结果中调用了9000多个write,但只有500个真正输出了数据,其余返回了错误,这种形式的循环是轮询,在多用户系统上会浪费CPU时间。

可以将程序设计成多线程的,避免使用非阻塞IO(一个线程在IO调用中阻塞),但线程间同步开销可能太高,最终可能得不偿失。

两人同时编辑一个文件时,大多UNIX系统中,文件最后状态取决于写该文件的最后一个进程。但数据库需要确保单独写一个文件,记录锁提供了进程独写一个文件的功能。

记录锁可以在一个进程正在读或修改文件的某部分时,阻止其他进程修改同一文件区。记录锁的更合适术语是字节范围锁,因为它锁定的只是文件中的一个区域(也可能是整个文件)。

早期伯克利版本支持flock函数,它只能对整个文件加锁。

SVR3通过fcntl函数增加了记录锁功能,在此基础上构造了lockf函数,可对文件中任意字节数的区域加锁,长至整个文件,短至一个字节。

POSIX.1记录锁的基础是fcntl:
在这里插入图片描述
用于记录锁的cmd参数是F_GETLK、F_SETLK、F_SETLKW。第三个参数是指向flock结构的指针:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第3张图片
flock结构说明:
1.l_type:所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域)。
2.l_whence和l_start:要加锁或解锁的起始字节偏移量。l_start基于l_whence计算偏移。
3.l_len:区域的字节长度。
4.l_pid:进程ID为pid持有的锁能阻塞当前进程。(仅由F_GETLK返回)

对于加锁或解锁区域:
1.l_whence的可选值与lseek函数相似,可选SEEK_SET、SEEK_CUR、SEEK_END。
2.锁可以在当前文件尾端或越过尾端处开始,但不能在文件起始位置之前开始。
3.若l_len为0,表示锁的范围为最大可能偏移量,此时不论向文件中追加了多少数据,它们都可以处于锁的范围内,不必猜测会有多少字节被追加到文件后,起始位置可以是文件中的任意一个位置。

从一个描述符读,然后又写到另一个描述符时的阻塞IO:

while ((n = read(STDIN_FILENO, buf, BUFSUZ)) > 0) {
    if (write(STDOUT_FILENO, buf, n) != n) {
        err_sys("write error");
    }
}

如果必须从两个描述符读,那么不能再任一描述符上进行阻塞读,否则可能会因为阻塞在一个描述符上的读操作导致另一个描述符即使有数据也无法处理。

telnet命令的结构:该程序从终端(标准输入)上读,将所得数据写到网络连接上,同时从网络连接读,将所得数据写到终端(标准输出)上。在网络另一端,telnetd守护进程将执行用户键入的命令,而命令产生的输出通过telnet命令送给用户,并显示在用户终端上:
在这里插入图片描述
telnet进程有两个输入、两个输出,我们不能使用阻塞read读任一输入,因为我们不知道哪个输入会得到数据。

处理这种问题的一个方法是,fork出另一个进程,每个进程处理一条数据通路:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第4张图片
这样两个进程都可执行阻塞read,但终止时比较麻烦,如果子进程收到文件结束符(telnetd守护进程使网络连接断开),那么该子进程终止,然后父进程收到SIGCHLD信号。如果是父进程终止(用户在终端上键入了文件结束符),则父进程应通知子进程终止,为此,可用一个信号(如SIGUSR1)。这使得程序更为复杂。

我们也可以用两个线程,这避免了终止的复杂性,但要求处理两个线程间的同步,复杂性提高。

另一个方法是使用两个非阻塞IO处理数据,然后先对一个描述符发出read,如无数据可读,则调用立即返回,然后对第二个描述符发出read。在此之后,等待一定时间,再从第一个描述符开始尝试读,这种形式的循环称为轮询。不足之处是浪费CPU时间,且每次循环后的等待时间也难以确定。在多任务系统中应避免使用此方法。

另一种技术是异步IO,进程告诉内核,当描述符准备好可以IO时,用一个信号通知它。但有两个问题,在POSIX前很多系统提供的异步IO无法移植;这种信号对每个进程而言只有一个,无法判断是哪个描述符准备好了,尽管POSIX异步IO接口允许选择哪个信号作为通知,但可用信号数远远少于潜在的打开描述符数量,为确定哪个描述符准备好,仍需将这两个描述符都设为非阻塞的,并顺序尝试IO。

比较好的技术是IO多路转接,为使用它,需要先构造一张描述符列表,然后调用一个函数,当描述符列表中的一个准备好IO时,函数才返回。

select是我们可以执行IO多路转接,select参数告诉内核:
1.描述符。
2.对于每个描述符我们关心的条件(是否想从一个给定描述符读、是否想向一个给定描述符写、是否关心一个给定描述符的异常条件)。
3.愿意等待多长时间。

select返回时,内核告诉我们:
1.已准备好IO的描述符数量。
2.对于读、写、异常这三个条件的每一个,哪些描述符已准备好。

UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第5张图片
最后一个参数tvptr指定愿意等待的时间长度,单位为s和us,它有三种情况:
1.tvptr == NULL:永远等待。捕捉到一个信号时中断等待,此时返回-1,errno被设为EINTR。
2.tvptr->tv_sec == 0 && tvptr->tv_usec == 0:不等待,测试所有指定描述符然后立即返回。这是轮询系统找到多个描述符状态而不阻塞select函数的方法。
3.tvptr->tv_sec != 0 || tvptr->tv_usec != 0:等待指定的秒数和微秒数。如果系统不支持us精度,则tvptr->tv_usec取整到最近的支持值。如果超时时没有一个描述符准备好,则返回值为0。可被捕捉到的信号中断。

select返回后,该结构的值可能已被改变。

参数readfds、writefds、exceptfds是指向描述符集的指针,说明了可读、可写或处于异常状态的描述符集合。描述符集存储在fd_set数据类型中,该类型具体实现由系统决定,可被看成:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第6张图片
fd_set类型可做的操作:分配一个该类型变量、将该类型变量值赋给同类型另一变量、对该类型变量使用以下函数:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第7张图片
FD_ZERO将一个fd_set中所有位置0;FD_SET可开启描述符集中一位;FD_CLR可清除一位;FD_ISSET测试描述符集中一个指定位是否打开。

函数操作方式:

fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(STDIN_FILENO, &rset);

从select返回时,可用FD_ISSET测试该集中一个给定位是否处于打开状态:

if (FD_ISSET(fd, &rset) ;

select的三个指针可以都是NULL,此时select是一个比sleep(秒级精度,select是系统时钟级精度)精度更高的定时器。

select的第一个参数maxfdp1含义为最大文件描述符编号+1,其值为三个描述符集中最大描述符编号值+1。也可将第一个参数值设为FD_SETSIZE,此常量位于头文件sys/select.h,它指定最大描述符数(常是1024)。内核通过我们提供的最大描述符,在此范围内寻找打开的位。

select返回值:
1.-1:出错,如指定描述符中一个都没准备好时捕捉到一个信号,此时,不修改描述符。
2.0:描述符没有一个准备好,指定的时间过了会发生,所有描述符集都置为0。
3.正数:已经准备好的描述符数。如果一个描述符已准备好读和写,返回值中会对其计数两次。

准备好的含义:
1.对于读集中的一个描述符调用read不会阻塞,则此描述符是准备好的。
2.对于写集中的一个描述符调用write不会阻塞,则认为此描述符是准备好的。
3.对于异常条件集中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。异常条件包括:网络连接上到达带外数据、处于数据包模式的伪终端上发生了某些条件。
4.对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

一个描述符阻塞与否不影响select是否阻塞。

如果一个描述符上碰到了文件尾端,则select认为该描述符是可读的,然后调用read,它返回0,这是UNIX系统指示到达文件尾端的方法。

POSIX.1定义了select的变体:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第8张图片
pselect与select的不同之处:
1.select超时值用timeval指定,而pselect用timespec指定,timespec提供s和ns精度,如果平台支持这样的时间精度,则timespec能提供更精确的超时时间。
2.pselect的超时值被声明为const的,保证不会改变此值。
3.pselect可屏蔽某些信号,调用pselect时,以原子方式安装该信号屏蔽字,返回时,恢复以前的信号屏蔽字。

poll类似于select:
在这里插入图片描述
但poll不为每个条件(可读、可写、异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及该描述符的条件:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第9张图片
fdarray数组中的元素数由超时nfds指定。

nfds_t最小能保存下一个int。

数组元素的events和revents成员可设置为以下值:
UNIX环境高级编程 学习笔记 第十四章 高级I/O(部分)_第10张图片
events成员告诉内核每个描述符对应的事件。返回时,revents成员由内核设置,用于说明每个描述符发生了哪些事件。

上图中前四行测试的是可读性、接下来三行测试的是可写性、最后三行测试的是异常条件。最后三行即使events字段中没有指定这三个值,如果相应条件发生,revents中也会返回它们。

当一个描述符被挂断,就不能再写该描述符,但仍可能从该描述符中读取到数据。

参数timeout可能值:
1.-1:永远等待。某些系统在stropts.h中定义了常量INFTIM,其值常为-1。当一个描述符已准备好,或捕捉到一个信号时返回。捕捉到信号时,poll返回-1,errno置为EINTR。
2.0:不等待,测试所有描述符后立即返回。轮询系统常用方法。
3.大于0:等待timeout ms。如果超时时还没有一个描述符准备好,则返回值为0。如果系统不支持毫秒级精度,则timeout取整到最近的支持值。

如果我们从终端输文件结束符,那么就会打开POLLIN,我们就可以读文件结束指示(read返回0),而revents的POLLHUP没有打开。如果我们在读调制解调器,并且电话线已挂断,将接到POLLHUP。

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

很多实现中,即使poll或select被信号中断且该信号设置了SA_RESTART标志,他们也不会重启动。

你可能感兴趣的:(UNIX环境高级编程(第三版))