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

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

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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函数应该只执行一次:
在这里插入图片描述
如果标准输出是终端,则write函数有时返回小于500000的一个数字,有时返回错误:
在这里插入图片描述
在该系统上,errno值35对应EAGAIN(在非阻塞模式下调用阻塞操作,没有完成就返回时会产生此错误)。终端驱动程序一次能接受的数据量随系统而变。

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

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

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

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

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

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

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

POSIX.1记录锁的基础是函数fcntl:
在这里插入图片描述
用于记录锁的cmd参数是F_GETLK(获取记录锁)、F_SETLK(设置记录锁)、F_SETLKW(设置记录锁,在F_SETLK条件下,如果设置锁时已经被加锁,则fcntl函数会立即返回,而此命令会令进程休眠)。第三个参数是指向flock结构的指针:
在这里插入图片描述
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:已经通过fcntl函数占有锁的进程ID(仅当cmd参数为F_GETLK时返回)。

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

任意多进程在一个给定字节上可以有一把共享的读文件锁,但在一个给定字节上只能有一个独占写文件锁。
在这里插入图片描述
以上兼容性规则适用于不同进程提出的锁请求,不适用于单个进程提出的多个锁请求,如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加锁时,新锁会替换已有锁。

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

fcntl函数用于文件锁时的cmd参数含义:
1.F_GETLK:判断由flockptr参数描述的锁是否会被阻塞,如果会被另一把锁阻塞,则把另一把现有锁的锁信息重写到flockptr参数;如果不会被另一把锁阻塞则将flockptr->l_type设置为F_UNLCK。
2.F_SETLK:设置由flockptr参数描述的锁,如果该锁会被阻塞,则fcntl函数立即返回出错,并将errno设为EACCES或EAGAIN(POSIX.1允许实现设置这两种errno中的一种)。
3.F_SETLKW:是F_SETLK的阻塞版本。如果加锁时被阻塞,进程会休眠,直到请求的锁可用或被信号中断。

F_GETLK用于测试能否建立一把锁,F_SETLK或F_SETLKW用于建立这把锁,但这两个操作不是原子操作。

POSIX.1没有说明以下情况会发生什么:对某文件的一个区间加写锁时,如果加读锁的请求来得很频繁,会使该文件区间时钟存在一把或几把读锁,导致欲加锁的进程等待很长时间。

设置或释放一把文件锁时,系统按要求组合或分裂相邻区,如第100~199字节是加锁的区,需解锁第150字节,则内核将维持两把锁:
在这里插入图片描述
如上图,假如我们又对第150字节加锁,则系统会将三个相邻加锁区合并成一个区,又跟开始时一样了。

为避免每次分配flock结构,然后又填入各项信息,可用以下函数处理这些细节:

#include <fcntl.h>

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len) {
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    return fcntl(fd, cmd, &lock);
}

大多调用都是加锁或解锁一个文件区域(F_GETLK很少用),通常使用以下五个宏中的一个:

#define read_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len) lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define write_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len) lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len) lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))

可用以下函数测试一把锁:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len) {
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    if (fcntl(fd, F_GETLK, &lock) < 0) {
        printf("fcntl error\n");
		exit(1);
    }

    if (lock.l_type == F_UNLCK) {
        return 0;    // false, region isn't locked by another proc
    }
    return lock.l_pid;    // true, return pid od lock owner
}

通常用以下宏调用以上函数:

#define is_read_lockable(fd, offset, whence, len) (lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0)
#define is_write_lockable(fd, offset, whence, len) (lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0)

lock_test函数不能用来测试调用进程自己是否在文件的某部分持有一把锁(F_GETLK不能报告调用进程自己持有的锁),一个进程可以任意替换自己在文件上已经加上的锁,不受读写锁规则限制。

发生文件锁死锁的例子:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define FILE_MODE 664

static void lockabyte(const char *name, int fd, off_t offset) {
    if (writew_lock(fd, offset, SEEK_SET, 1) < 0) {
        printf("%s: writew_lock error\n", name);
		exit(1);
    }
    printf("%s: got the lock, byte %lld\n", name, (long long)offset);
}

int main() {
    int fd;
    pid_t pid;

    // create a file and write two bytes to it
    if ((fd = creat("templock", FILE_MODE)) < 0) {
        printf("create error\n");
		exit(1);
    }
    if (write(fd, "ab", 2) != 2) {
        printf("write error\n");
		exit(1);
    }

    TELL_WAIT();
    if ((pid = fork()) < 0) {
        printf("fork error\n");
    } else if (pid == 0) {    // child
        lockabyte("child", fd, 0);
		TELL_PARENT(getppid());
		WAIT_PARENT();
		lockabyte("child", fd, 1);
    } else {    // parent
        lockabyte("parent", fd, 1);
		TELL_CHILD(pid);
		WAIT_CHILD();
		lockabyte("parent", fd, 0);
    }
    exit(0);
}

运行它:
在这里插入图片描述
检测到死锁时,内核选择一个进程接收出错返回,具体选择取决于实现。

关于记录锁的自动继承和释放的三条规则:
1.一个进程终止时,它锁建立的锁全部释放;一个描述符关闭时,该描述符引用的文件上的所有该进程设置的锁被释放,而不管进程是否是通过此描述符在该文件上加的锁。
2.fork函数产生的子进程不继承父进程的锁。对于通过fork从父进程继承来的描述符,子进程需要调用fcntl函数才能获得它自己的锁。如果不是这样,则父子进程可对同一个文件加写锁,从而同时写一个文件。
3.调用exec后,新程序继承原执行程序的锁。如果对一个文件描述符设置了执行时关闭标志,则exec函数关闭该文件描述符时,将释放相应文件上的锁。

观察FreeBSD上文件锁的实现中使用的数据结构,考虑执行以下语句的进程:
在这里插入图片描述
以下是父子进程都调用了pause后的数据结构情况:
在这里插入图片描述
文件的i节点中有一个指向lockf结构的指针,lockf结构中有一个lockf_entry结构的链表,每向该文件上加一个文件锁,lockf_entry结构链表中就多一项,lock_entry结构中包含相应进程的ID,如果在父进程中关闭fd1、fd2、fd3三个描述符中的任一个,内核会从该描述符关联的i节点开始,逐个检查lockf_entry结构链表中的各项,并释放父进程持有的锁,内核不知道也不关心父进程是用这三个描述符中的哪个来设置这把锁的。

守护进程可用文件锁保证该守护进程只有一个副本在运行,可用以下函数在文件上加写锁:

#include <unistd.h>
#include <fcntl.h>

int lockfile(int fd) {
    struct flock f1;

    f1.l_type = F_WRLCK;
    f1.l_start = 0;
    f1.l_whence = SEEK_SET;
    f1.l_len = 0;

    return fcntl(fd, F_SETLK, &f1);
}

也可用write_lock宏来定义lockfile函数:

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

在对文件尾端字节范围加锁时,不能使用fstat函数得到当前文件长度,因为在fstat调用和加锁之间可能有其它进程改变文件长度。

考虑以下代码序列:

writew_lock(fd, 0, SEEK_END, 0);
write(fd, buf, 1);    // 假定此时文件偏移量在文件尾处
un_lock(fd, 0, SEEK_END);
write(fd, buf, 1);

加锁时,内核会将相对偏移量变换成绝对偏移量,因此加锁和解锁的绝对偏移量是不同的,因此最终锁还是加在第一次write还是写的那个字节上:
在这里插入图片描述
如想把全部锁都解除,以上代码中的unlock函数应这样调用:

un_lock(fd, -1, SEEK_END);

考虑数据库访问例程库,如果库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集为合作进程。如果这些函数是唯一的用来访问数据库的函数,则它们使用建议锁是可行的。但建议锁不能阻止其他对数据库文件有写权限的进程写已经加锁的文件。不使用数据库访问例程库来协同一致地访问数据库的进程是非合作进程。

强制性锁会让内核检查每个open、read、write函数,验证调用进程是否违背了文件上的某把锁。强制性锁也称强迫方式锁。

Linux 3.2.0和Solaris10提供强制性记录锁,FreeBSD 8.0和Mac OS X 10.6.8不提供。强制性记录锁不是SUS的组成部分,在Linux中,如果用户想要使用强制性锁,需要在各个文件系统上用mount命令的-o mand选项打开该机制。

对一个特定文件打开其设置组ID位,关闭其组执行位便开启了对该文件的强制性锁机制,因为当组执行位关闭时,设置组ID位不再有意义,所以SVR 3的设计者用这一组合指定对一个文件的锁是强制性的而非建议性的。

如果一个进程试图读或写一个强制性锁起作用的文件,结果如下:
在这里插入图片描述
open一个强制性锁起作用的文件会成功,但后续的read和write会依从上图规则。但如果open函数的标志有O_TRUNC,则不论是否指定了O_NONBLOCK,open函数都立即出错返回,并将errno设为EAGAIN。只有Solaris对open函数以O_CREAT打开具有强制性锁文件时返回出错(也会设置errno为EAGAIN)。

用某些UNIX系统程序和操作符对加了强制性读锁的文件进行处理,发生了以下情况:
1.ed编辑器可对编辑该文件,且结果可写回磁盘,通过对ed操作进行跟踪发现,ed将新文件内容写到一个临时文件中,然后删除原文件,最后将临时文件名改为原文件名,而强制性锁机制对unlink函数没有影响。FreeBSD 8.0和Solaris 10中,用truss命令可得到一个进程的系统调用跟踪信息;Linux 3.2.0使用strace命令;Mac OS X 10.6.8使用dtruss命令,但该命令需要超级用户权限。
2.不能用vi编辑器编辑该文件,但可读。将新的数据写到文件中时,会出错返回;将新数据追加写到文件中时,write函数会阻塞。
3.使用Korn shell的>和>>操作符重写或追加写该文件时,会产生错误信息connot create。
4.在Bourne shell下使用>操作符也会出错,但使用>>操作符时只会阻塞,在强制性锁解除后会继续处理。与3产生差异原因在于,Korn shell以O_CREAT和O_APPEND标志打开文件,指定O_CREAT时会出错返回,而Bourne shell在文件已存在时不指定O_CREAT,所以open成功,而下一个write调用会阻塞。

以上操作结果取决于操作系统版本。

恶意用户可对一个文件加强制锁,阻止其他人写此文件。

确定一个系统是否支持强制性锁:

#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <stdio.h>

#define FILE_MODE 664

int main(int argc, char *argv[]) {
    int fd;
    pid_t pid;
    char  buf[5];
    struct stat statbuf;

    if (argc != 2) {
        fprintf(stderr, "usage: %s filename\n", argv[0]);
		exit(1);
    }
    if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0) {
        printf("open error\n");
		exit(1);
    }
    if (write(fd, "abcdef", 6) != 6) {
        printf("write error\n");
		exit(1);
    }

    // turn on set-group-ID and turn off group-execute
    if (fstat(fd, &statbuf) < 0) {
        printf("fstat error\n");
		exit(1);
    }
    if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) {
        printf("fchmod error\n");
		exit(1);
    }

    TELL_WAIT();

    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid > 0) {    // parent
        // write lock entire file
		if (write_lock(fd, 0, SEEK_SET, 0) < 0) {
		    printf("write_lock error\n");
		    exit(1);
		}
	
		TELL_CHILD(pid);
	
		if (waitpid(pid, NULL, 0) < 0) {
		    printf("waitpid error\n");
		    exit(1);
		}
    } else {    // child
        WAIT_PARENT();    // wait for parent to set lock

		set_fl(fd, O_NONBLOCK);    // set O_NONBLOCK to fd
	
		// first let's see what error we get if region is locked
		if (read_lock(fd, 0, SEEK_SET, 0) != -1) {    // no wait
		    printf("child: read_lock succeeded\n");
		}
		printf("read_lock of already-locked region returns %d\n", errno);
	
		// now try to read the mandatory lock file
		if (lseek(fd, 0, SEEK_SET) == -1) {
		    printf("lseek error\n");
		    exit(1);
		}
		if (read(fd, buf, 2) < 0) {
		    printf("read failed  (mandatory locking works)\n");
		    exit(0);
		} else {
		    printf("read OK (no mandatory locking), buf = %2.2s\n", buf);
		}
    }

    exit(0);
}

以上程序先创建一个文件,并使强制性锁机制对其起作用,然后进程fork出一个子进程,在父进程中对整个文件加一把写锁,子进程先将该文件的描述符设为非阻塞的,然后试图对整个文件设置一个读锁,我们期望这会出错返回,并希望看到errno被设为EACCES或EAGAIN,接着,子进程将读写位置调整到文件起点,并试图读文件,如果系统提供强制性锁机制,read函数应返回EACCES或EAGAIN,否则read函数返回所读数据。在支持强制性锁机制的Solaris 10上运行以上程序:
在这里插入图片描述
该系统中,errno的值11表示EAGAIN。若在FreeBSD 8.0上运行以上程序:
在这里插入图片描述
该系统中,errno的值35表示E_AGAIN。

某些版本的vi编辑器使用建议性记录锁,它不能阻止其他用户使用另一个没有使用建议性记录锁的编辑器。如果系统提供强制性记录锁,可修改编辑器源码使其使用强制性锁,如果没有编辑器源码,可编写一个vi的前端程序,该程序开始时立即调用fork,然后父进程只等待子进程完成,子进程打开命令行中文件,使强制性锁起作用,然后对整个文件加一把写锁,之后再执行vi,执行vi时,该文件是加了写锁的,其他用户不能修改它,vi结束时,父进程从wait函数返回,自编的前端程序结束。但这种自编的程序可能不起作用,因为大多编辑器读它们的输入文件,然后关闭它,锁也就释放了。

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

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

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

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

处理这种问题的一个方法是,fork出另一个进程,每个进程处理一条数据通路:
在这里插入图片描述
这样两个进程都可执行阻塞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.对于读、写、异常这三个条件的每一个,哪些描述符已准备好。

在这里插入图片描述
最后一个参数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函数返回后,参数tvptr指向的timeval结构的值会被修改为剩余等待时间。

参数readfds、writefds、exceptfds是指向描述符集的指针,说明了可读、可写或处于异常状态的描述符集合。描述符集存储在fd_set数据类型中,该类型具体实现由系统决定,可被看成:
在这里插入图片描述
fd_set类型可做的操作:分配一个该类型变量、将该类型变量值赋给同类型另一变量、对该类型变量使用以下函数:
在这里插入图片描述
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函数返回准备好的描述符的数量,但哪个准备好了并不确定,需要遍历一遍已有的所有描述符,对每个描述符调用FD_ISSET查看该描述符是否已经准备好,如传给select函数一个读fd_set,其中有3个描述符,当select函数返回1时,说明有1个描述符准备好被读了,之后对这3个描述符中的每一个调用FD_ISSET(fd, &rset),如果返回true,说明此描述符可读了,写和异常条件也是这样。

上述FD_ISSET(fd, &rset)代码的含义是,查看fd是否在rset中,但我们将fd_set参数(此例中为rset)传给select函数时,rset中应该包含了全部3个fd,这样此代码应该永远返回的都是true。这样能工作的原因在于,select函数会修改传入的rset参数,在返回时,select函数已经将rset改为所有准备好的fd,因此每次调用select前,我们都需要重置一次rset,就像下面代码一样:

FD_SET(sockfd, &fd_set_init);
while(true) {
    FD_COPY(&fd_set_init, &fd_set);
    n = select(nfds, &fd_set, NULL, NULL, NULL);
    if (n > 0) {
        // Use fd_set here
    }
}

fd_set的结构定义如下:

typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
} fd_set;

在select函数返回时,fd_set的fd_count和fd_array成员都可能被修改。

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函数是否阻塞。

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

POSIX.1定义了select函数的变体:
在这里插入图片描述
pselect函数与select函数的不同之处:
1.select函数超时值用timeval结构体指定,而pselect函数用timespec结构体指定,timespec结构体提供s和ns精度,如果平台支持这样的时间精度,则timespec结构体能提供更精确的超时时间。
2.pselect函数的超时值被声明为const的,保证不会改变此值。
3.pselect函数可屏蔽某些信号,调用pselect时,以原子方式安装该信号屏蔽字,返回时,恢复以前的信号屏蔽字。

poll函数类似于select函数:
在这里插入图片描述
但poll函数不为每个条件(可读、可写、异常条件)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及该描述符的条件:
在这里插入图片描述
参数fdarray数组中的元素数由参数nfds指定。

nfds_t类型最小能保存下一个int。

参数fdarray数组中的元素的events和revents成员可设置为以下值:
在这里插入图片描述
events成员告诉内核监听每个描述符对应的事件。返回时,revents成员由内核设置,用于说明每个描述符发生了哪些事件。

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

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

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

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

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

很多实现中,即使poll函数或select函数被信号中断且该信号设置了SA_RESTART标志,这两个函数也不会重启动。 但在SVR 4及其派生系统上,指定了SA_RESTART标志后会重启,为了在将软件移植到SVR 4派生的系统上时阻止这一点,当程序运行在SVR 4上时,需要设置SA_INTERRUPT标志。

select和poll函数是一种异步形式的通知,系统不告诉我们任何信息,直到我们调用select或poll查询。信号提供一种异步形式的通知,所有派生自BSD和System V的系统都提供异步IO,这些异步IO都是使用某种形式的信号来通知描述符上发生了什么我们感兴趣的事(System V使用SIGPOLL,BSD使用SIGIO),但这些形式的异步IO有以下限制,它们不能作用于所有类型的文件,且只能使用一个信号。如果我们同时对多个文件描述符进行异步IO,进程接收到信号时就不能分辨出信号对应于哪个文件描述符。

SUSv4将异步的IO机制从实时扩展移动到基础规范部分,这解决了旧的异步IO设施中存在的限制。

使用异步IO时,同时应付多个并发操作会使程序变得复杂,一个简单的方法是使用多线程,这使得我们可以以同步的方式写程序,再使各个线程异步运行(一个线程处理信号,其他线程无视信号的异步性即可)。

POSIX IO接口带来了以下复杂性:
1.每个异步IO操作都有以下三个出错的地方需要注意:IO操作呈递过来时、IO操作本身、确定异步IO操作状态的函数中。
2.相比传统IO,接口本身进行了很多其他设置,处理了很多规则。(我们不能称非异步IO为同步IO,虽然read、write函数它们对于程序流来说是同步的,但对于IO来说不一定是同步的,只有当write函数返回时写入的内容已经持久化了,我们才称其为同步写;我们也不能将read、write等函数称为标准IO来跟异步IO区分,这样会与标准IO库相混淆,因此本章称write、read等函数为传统IO)
3.从错误中恢复是困难的,例如,如果呈递过来多个异步写,但一个失败了,如果这些写是相关联的,我们可能必须撤销已经成功的写。

各种异步IO的实现:
1.System V
System V提供的异步IO是有限制的,只能用于STREAMS设备和STREAMS管道。System V异步IO使用的信号是SIGPOLL。

System V上为了启用STREAMS设备的异步IO,需要以I_SETSIG为resqust参数调用ioctl,此时第三个参数是下图中的一个或多个常量组成的整型值,这些整型值定义在stropts.h头文件中:
在这里插入图片描述
与STREAMS机制相关的接口在SUSv4中已被标记为废弃的。

除了调用ioctl来指定SIGPOLL信号产生的条件外,还需要为SIGPOLL设置信号处理程序,此信号的默认动作是终止进程。

2.BSD
派生自BSD的系统的异步IO使用了SIGIO和SIGURG信号的组合,SIGIO是异步IO信号,SIGURG用来通知进程网络连接上有带外数据到来。

为了接收SIGIO信号,需要以下三步:
1.使用sigaction或signal函数为SIGIO建立信号处理程序。
2.以F_SETOWN为参数调用fcntl来设置接收该描述符上异步IO信号的进程ID或进程组ID。
3.以F_SETFL为参数调用fcntl将O_ASYNC设置到文件状态标志位,从而打开此文件描述符上的异步IO。这一步只能用于终端或网络描述符上,这是BSD异步IO设施的限制。

为了接收到SIGURG信号,只需设置以上的1、2步,此信号只会在支持带外数据的网络描述符(如TCP连接)上产生。

3.POSIX
POSIX异步IO接口使我们以一致的方式处理所有类型文件上的异步IO,这些接口来自SUS的实时草案标准选项,在SUSv4中,这些接口被移到基本部分中,现在所有平台都要求支持这些接口。

POSIX异步IO接口使用AIO控制块来描述IO操作,aiocb结构定义一个AIO控制块,它至少包括以下字段:
在这里插入图片描述
aio_fildes字段是用来读写的文件描述符。读写开始在aio_offset字段。对于读,数据被复制到aio_buf字段的开头;对于写,是将aio_buf字段中内容写出。读写的字节数包含在aio_nbytes字段中。

异步IO操作必须明确指定偏移量。异步IO接口不会影响操作系统中的文件偏移量,只要我们不在一个程序中混用异步IO和传统IO就没问题。如果我们向O_APPEND打开的文件中异步写,aio_offset字段会被系统忽略。

aio_reqprio字段给程序一个建议的异步IO请求执行顺序,系统对准确的顺序的控制有限,不能确保该顺序被遵守。

aio_lio_opcode字段只用于基于列表的异步IO。

aio_sigevent字段控制IO实现完成后程序如何被通知,该字段类型为sigevent结构:
在这里插入图片描述
sigev_notify字段控制通知类型,可取值如下:
1.SIGEV_NONE:异步IO请求完成时不通知进程。
2.SIGEV_SIGNAL:当异步IO请求完成时产生sigev_signo字段指定的信号。如果应用选择捕捉此信号,且设置信号处理程序时指定了SA_SIGINFO标志(这会使一个si_value字段被设为sigev_value的siginfo结构被传给信号处理程序),则此信号会排队(如果实现支持排队信号)。
3.SIGEV_THREAD:当异步IO完成时,由sigev_notify_function字段指定的函数会被调用,由sigev_value字段指定的参数会作为唯一的参数被传给该函数。这个函数会在新的线程中以分离状态执行,除非sigev_notify_attributes字段被设置为一个pthread线程属性结构的地址,新线程的属性会被设为这个线程属性结构表示的属性。

为执行异步IO,我们需要初始化一个AIO控制块,然后调用以下函数进行异步读写:
在这里插入图片描述
当以上函数成功返回时,异步IO请求已经被操作系统加到待处理队列中了,返回值和实际IO操作的执行结果不相关。当IO操作未做时,必须保证AIO控制块和数据缓冲区内容不变,在IO操作完成前,不能重用它们。

为强制将一个文件上的所有异步IO写同步写到磁盘,我们可以创建一个AIO控制块,并调用以下函数:
在这里插入图片描述
参数aiocb的aio_fildes字段指示想同步写的文件描述符。如果op参数是O_DSYNC,则作用与调用fdatasync相同;如果op参数是O_SYNC,则作用与调用fsync相同。

就像aio_read和aio_write函数,aio_fsync函数也只是一个请求,不会等待IO结束。只有异步IO同步完成,数据才是持久化的。AIO控制块也控制着在IO结束后我们如何被通知。

为确定异步读、写、同步的完成状态,需要调用以下函数:
在这里插入图片描述
返回值如下:
1.0:异步操作成功完成,我们需要调用aio_return来获得操作的返回值。
2.-1:aio_error函数调用失败,errno告诉我们为什么失败。
3.EINPROGRESS:异步读、写、同步还是未决的。
4. 其他返回值:异步操作失败,返回错误码。

如果异步操作成功,调用以下函数获取异步操作的返回值:
在这里插入图片描述
异步操作完成前,应避免调用aio_return,因为此时的返回值是未定义的。对于每个异步IO操作,aio_return函数只能调用一次,一旦我们调用了此函数,操作系统就认为包含IO操作返回值的记录是可删除的了。

当aio_return函数本身调用失败时,会返回-1并设置errno。否则它会返回异步操作的结果,此时,它会返回read或write或fsync函数调用成功时的返回值。

当我们不想在执行IO时被阻塞而有其他事要做时,使用异步IO。如果我们已经完成了要做的事,但发现仍然有异步操作未完成,我们可以调用以下函数来阻塞自己直到list参数中的某个操作完成:
在这里插入图片描述
aio_suspend会在以下三种条件下返回:
1.被信号终端时,此时会返回-1并设置errno为EINTR。
2.如果可选参数timeout超时,且没有任何操作完成,此时会返回-1并设置errno为EAGAIN。如果不想有挂起时间限制,将空指针传给timeout参数。
3.如果任何一个操作完成,返回0。如果调用此函数时所有IO操作都完成了,那么会立即返回。

list参数是指向AIO控制块数组的指针,nent参数指明了数组中的元素个数。此数组中的空指针元素会被跳过,非空元素必须已经用于初始化IO操作。

当我们想取消未决的异步IO操作,可尝试调用以下函数来取消(尝试的含义是系统不保证成功取消正在进行的异步IO):
在这里插入图片描述
fd参数指定了未完成的异步IO操作的描述符。如果aiocb参数是NULL,则操作系统尝试取消此文件描述符上所有的未完成异步IO操作,如果不是NULL,则系统尝试取消此参数表示的AIO控制块指定的单个异步IO操作。

aio_cancel函数返回值:
1.AIO_ALLDONE:所有的操作已经完成,没有取消任何操作。
2.AIO_CANCELED:所有的操作都被取消。
3.AIO_NOTCANCELED:至少一个操作没有被取消。
4.-1:aio_cancel调用失败,errno被设置。

如果一个异步IO操作成功取消,则相应AIO控制块的aio_error调用会返回ECANCELED;如果操作没有被取消,则相应的AIO控制块不会被修改。

以下函数是异步IO接口中的附加接口,可用于异步或同步方式的IO:
在这里插入图片描述
list参数是AIO控制块数组表示的一组IO请求,nent参数指定数组中的元素数。AIO控制块数组中可以有NULL指针,这些元素会被忽视。

mode参数决定IO是否真的是异步的,当它被设为LIO_WAIT时,函数会在list参数指定的所有IO操作都完成后返回,这种情况下,sigev参数是无意义的;当它被设为LIO_NOWAIT时,函数会在list参数指定的所有IO操作被加入队列后返回,此时进程会在所有异步IO操作都完成后被通知,通知方式由sigev参数指定,如果我们不想被通知,将sigev设为NULL,但单个的AIO控制块在相应的单个请求完成时也能启用异步通知。

在每个AIO控制块中,aio_lio_opcode字段指明操作是读(LIO_READ)、写(LIO_WRITE)或没有操作(LIO_NOP)。读相当于将AIO控制块传给aio_read函数,写相当于将AIO控制块传给aio_write函数。

实现会限制未完成的异步IO操作数量,此限制是运行时不变量:
在这里插入图片描述
我们可以用sysconf函数获取上表中三个运行时不变量:用_SC_IO_LISTIO_MAX为name参数调用sysconf获取AIO_LISTIO_MAX;用_SC_AIO_MAX为name参数调用sysconf获取AIO_MAX;用_SC_AIO_PRIO_DELTA_MAX为name参数调用sysconf获取AIO_PRIO_DELTA_MAX。

POSIX异步IO接口最初目的是为了为实时应用提供一种方法来避免执行IO时的阻塞。

使用POSIX异步IO接口的例子:为了与传统IO比较,以下执行一个将文件格式转换为另一种的任务,其中有使用了USENET新闻系统使用的ROT-13算法的文件,该算法用于将含有冒犯性的或剧透的或笑话笑点的信息模糊掉,此算法将a到z和A到Z的字符循环向右移13个位置,其他字符不变。以下是传统IO完成此任务的代码:

#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

#define BSZ 4096

unsigned char buf[BSZ];

unsigned char translate(unsigned char c) {
    if (isalpha(c)) {
        if (c >= 'n') {
		    c -= 13;
		} else if (c >= 'a') {
		    c += 13;
		} else if (c >= 'N') {
		    c -= 13;
		} else {
		    c += 13;
		}
    }

    return c;
}

int main(int argc, char *argv[]) {
    int ifd, ofd, i, n, nw;

    if (argc != 3) {
        printf("usage: rot13 infile outfile\n");
		exit(1);
    }
    if ((ifd = open(argv[1], O_RDONLY)) < 0) {
        printf("can't open %s\n", argv[1]);
		exit(1);
    }
    if ((ofd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664)) < 0) {
        printf("can't create %s\n", argv[2]);
        exit(1);
    }

    while ((n = read(ifd, buf, BSZ)) > 0) {
        for (i = 0; i < n; ++i) {
		    buf[i] = translate(buf[i]);
		}
	
		if ((nw = write(ofd, buf, n)) != n) {
		    if (nw < 0) {
		        printf("write failed\n");
				exit(1);
		    } else {
		        printf("short write (%d/%d)\n", nw, n);
		        exit(1);
		    }
		}
    }

    fsync(ofd);
    exit(0);
}

以上程序的IO部分很直接:从输入文件中读一部分,翻译它,然后写到输出文件中,重复以上步骤直到到达文件尾,此时read函数返回0。以下程序使用异步IO完成相同任务:

#include <ctype.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <aio.h>
#include <errno.h>

#define BSZ 2 
#define NBUF 8

enum rwop {
    UNUSED = 0,
    READ_PENDING = 1,
    WRITE_PENDING = 2
};

struct buf {
    enum rwop op;
    int last;
    struct aiocb aiocb;
    unsigned char data[BSZ];
};

struct buf bufs[NBUF];

unsigned char translate(unsigned char c) {
    if (isalpha(c)) {
        if (c >= 'n') {
            c -= 13;
		} else if (c >= 'a') {
		    c += 13;
		} else if (c >= 'N') {
		    c -= 13;
		} else {
		    c += 13;
		}
    }

    return c; 
}

int main(int argc, char *argv[]) {
    int ifd, ofd, i, j, n, err, numop;
    struct stat sbuf;
    const struct aiocb *aiolist[NBUF];
    off_t off = 0;

    if (argc != 3) {
        printf("usage: rot13 infile outfile\n");
		exit(1);
    }
    if ((ifd = open(argv[1], O_RDONLY)) < 0) {
        printf("can't open %s\n", argv[1]);
		exit(1);
    }
    if ((ofd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664)) < 0) {
        printf("can't create %s\n", argv[2]);
		exit(1);
    }
    if (fstat(ifd, &sbuf) < 0) {
        printf("fstat failed\n");
		exit(1);
    }

    // initialize the buffers
    for (i = 0; i < NBUF; ++i) {
        bufs[i].op = UNUSED;
		bufs[i].aiocb.aio_buf = bufs[i].data;
		bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;
		aiolist[i] = NULL;
    }

    numop = 0;
    for (; ; ) {
        for (i = 0; i < NBUF; ++i) {
		    switch (bufs[i].op) {
		    case UNUSED:
			    // read from the input file if more data remains unread
				if (off < sbuf.st_size) {
				    bufs[i].op = READ_PENDING;
				    bufs[i].aiocb.aio_fildes = ifd;
				    bufs[i].aiocb.aio_offset = off;
				    off += BSZ;
				    if (off >= sbuf.st_size) {
				        bufs[i].last = 1;
				    }
				    bufs[i].aiocb.aio_nbytes = BSZ;
				    if (aio_read(&bufs[i].aiocb) < 0) {
				        printf("aio_read failed\n");
						exit(1);
				    }
				    aiolist[i] = &bufs[i].aiocb;
				    numop++;
				}
				break;

            case READ_PENDING:
		        if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) {
				    continue;
				}
				if (err != 0) {
				    if (err == -1) {
				        printf("aio_error failed\n");
						exit(1);
				    } else {
				        printf("read failed\n");
						exit(1);
				    }
				}
		
				// aread is complete; translate the buffer and write it
				if ((n = aio_return(&bufs[i].aiocb)) < 0) {
				    printf("aio_return failed\n");
				    exit(1);
				}
				if (n != BSZ && !bufs[i].last) {
				    printf("short read (%d/%d)\n", n, BSZ);
				    exit(1);
				}
				for (j = 0; j < n; ++j) {
				    bufs[i].data[j] = translate(bufs[i].data[j]);
				}
				bufs[i].op = WRITE_PENDING;
				bufs[i].aiocb.aio_fildes = ofd;
				bufs[i].aiocb.aio_nbytes = n;
				if (aio_write(&bufs[i].aiocb) < 0) {
				    printf("aio_write failed\n");
				    exit(1);
				}
				// retain our spot in aiolist
				break;

		    case WRITE_PENDING:
		        if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS) {
				    continue;
	    		}
				if (err != 0) {
				    if (err == -1) {
				        printf("aio_error failed\n");
						exit(1);
				    } else {
				        printf("write failed\n");
				    }
				}
	
				// a write is complete, mark the buffer as unuses
				if ((n = aio_return(&bufs[i].aiocb)) < 0) {
				    printf("aio_return failed\n");
				    exit(1);
				}
				if (n != bufs[i].aiocb.aio_nbytes) {
				    printf("short write (%d/%d)\n", n, BSZ);
				    exit(1);
				}
				aiolist[i] = NULL;
				bufs[i].op = UNUSED;
				numop--;
				break;
		    }
		}

		if (numop == 0) {
		    if (off >= sbuf.st_size) {
		        break;
		    }
		} else {
	        if (aio_suspend(aiolist, NBUF, NULL) < 0) {
	            printf("aio_suspend failed\n");
				exit(1);
		    }
		}
	}
	
    bufs[0].aiocb.aio_fildes = ofd;
    if (aio_fsync(O_SYNC, &bufs[0].aiocb) < 0) {
        printf("aio_fsync failed\n");
		exit(1);
    }

    exit(0);
}

以上代码使用了8个缓冲区,因此最多有8个异步IO处于等待状态。以上异步IO的做法可能会降低性能,当读操作以乱序传给文件系统时,会使操作系统的提前读算法失效。

在我们获取异步IO操作的返回值前,需要确保操作已经完成了。aio_error函数返回-1和EINPROGRESS以外的值时,操作是已经完成的,此时如果返回值非0,则操作是失败的。当我们已经检查完这些条件后,调用aio_return获取异步IO操作的返回值才是安全的。

当有一个未使用的AIO控制块时,我们就可以提交一个异步读请求,当读请求完成时,我们改变缓存内容,然后提交一个异步写请求,当所有AIO控制块都在使用中时,我们调用aio_suspend等待一个操作完成。

当我们向输出文件中写一个块内容时,我们保持从文件中读时的偏移量,因此,写的顺序不重要。这个策略能运行的原因是输入文件中的每个字母对应的输出文件中的字母都是相同偏移。

上例中我们没有使用异步通知,使用同步编程模型更简单。当我们在IO操作期间要做一些额外工作时,这些工作可加入到for循环中。如果我们不想让额外工作延迟翻译文件的任务,我们需要重新组织代码来使用某种形式的异步通知。如果有多个任务,在决定程序如何架构前需要先决定任务的优先级。

readv和writev函数用于在一个函数调用中读或写多个非连续的缓冲区,这些操作被称作分散读和聚集写:
在这里插入图片描述
iov参数是指向一个iovec结构数组的指针,数组中元素数由参数iovcnt指明,最大值为IOV_MAX。
在这里插入图片描述
传给readv和writev函数的iovec数组结构:
在这里插入图片描述
writev函数以以下方式合并输出数据:iov[0],iov[1]…iov[iovcnt - 1],函数返回值是输出的字节数,该字节总数通常应该等于所有的缓冲区长度和。readv函数将读到的数据按上述同样的顺序散布到缓冲区中,总是先填满一个缓冲区,然后再填下一个,readv函数返回读到的字节总数,如遇到文件尾,返回0。

以上两函数始于4.2 BSD,后来SVR4也提供它们,SUS的XSI扩展中也包括了这两个函数。

将两个缓冲区中的内容连续地写到一个文件中,有三种方式实现:
1.调用两次write,每个缓冲区一次。
2.分配一个大到足以包含两个缓冲区的新缓冲区,将两个缓冲区的内容复制到新缓冲区中,然后对这个新缓冲区调用一次write。
3.调用writev输出两个缓冲区。

第一个缓冲区大小为100字节,第二个缓冲区大小为200字节,重复将其中内容输出到文件,产生了一个300MB的文件,以下是三种方法所用时间,单位为秒:
在这里插入图片描述
调用两次write的系统时间比调用一次write或writev长。一次write的时间少于一次writev的时间,这是由于两个缓冲区较小,writev的固定成本大于收益,随着复制数据增加,writev函数性能将更好。

管道、FIFO及某些设备(终端、网络等)有以下性质:
1.一次read调用返回的数据可能少于要求的数据,即使还没达到文件尾,这不是一个错误,应继续读该设备。
2.一次write调用的返回值也可能少于指定输出的字节数,这可能是内核输出缓冲区满等因素造成的,这也不是错误,应继续写余下的数据。(通常只有非阻塞描述符,或捕捉到一个信号时,才会发生这种write函数的中途返回)

读写磁盘文件时没有以上情况,除非文件系统用完了空间,或接近了配额限制,不能将要求写的全部数据写出。

以下函数读、写指定的N字节数据,并处理返回值可能小于要求值的情况,它们只是按需多次调用read、write直至读写了N字节数据:
在这里插入图片描述
以上两函数是自定义的,不是哪个标准的组成部分。

当要将数据写到以上文件类型时,可使用writen函数;只有当事先知道要接收数据的字节数时,才使用readn函数。

writen和readn函数的实现:

#include <unistd.h>

ssize_t readn(int fd, void *ptr, size_t n) {
    size_t nleft;
    ssize_t nread;

    nleft = n;
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
		    if (nleft == n) {
		        return -1;    // error
		    } else {
		        break;    // error, return amount read so far
		    }
		} else if (nread == 0) {
		    break;    // EOF
		}
		nleft -= nread;
        ptr += nread;
    }

    return n - nleft;    // return >= 0
}

ssize_t writen(int fd, const void *ptr, size_t n) {
    size_t nleft;
    ssize_t nwritten;

    nleft = n;
    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) < 0) {
		    if (nleft == n) {
		        return -1;    // error
		    } else {
		        break;    // error, return amount written so far
		    }
		} else if (nwritten == 0) {
	        break;    // error, return amount written so far
		}
		nleft -= nwritten;
        ptr += nwritten;	
    }

    return n - nleft;    // return >= 0
}

size_t是一个无符号整型值,它的具体类型取决于实现,可以用来表示任意对象的大小,包括数组,常用于数组下标,这样用有些奇怪,因为数组下标不是一个对象的大小,但考虑极端的一种情况:数组大小是对象最大的可能大小,其中一个元素的大小是1字节,此时数组中的最大下标就是最大可能的对象的字节数-1,准确地说是SIZE_MAX。有人说size_t可用于内存中对象的计数,这假设了最大可能的对象大小是整个地址空间,这可能不正确。

ssize_t可以读作signed size_t,它的作用与size_t相同,但它能表示-1,很多系统调用使用-1来表示错误。

以上两个函数如果在读写了一部分数据后出错,则返回值是已经传输的数据量。

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

存储映射IO伴随虚拟存储系统已经用了很多年,4.1 BSD用vread和vwrite函数提供了一种不同形式的存储映射IO,但在4.2 BSD中删除了这两个函数,试图换成mmap函数,但4.2 BSD中并没有包含mmap函数。SUSv4把mmap函数从可选规范中移到了基础规范中,所有遵循POSIX的系统都要支持mmap函数。

为使用存储映射IO,先要使用以下函数告诉内核将一个给定文件映射到一个存储区域中:
在这里插入图片描述
addr参数指定映射存储区的起始地址,通常将其设为0,表示让系统选择存储映射区的起始地址。函数返回值是存储映射区的起始地址。

fd参数指定要被映射文件的描述符,文件映射到地址空间前,必须先打开。

len参数指定要映射的字节数;off参数指定要映射的字节在文件中的起始偏移量。

prot参数指定了映射区的访问级别:
在这里插入图片描述
我们可以将prot参数设为PROT_NONE或其余三个的任意组合的或,但不能超过文件open时的访问权限。

下图是一个内存映射文件的内存布局:
在这里插入图片描述
上图中的start addr时mmap函数的返回值。被映射的文件在堆和栈中间,这是取决于实现的。

flag参数可取值:
1.MAP_FIXED:函数返回值等于addr参数。此值影响可移植性,不推荐使用。如果没有指定此值,且addr参数非0,则系统将addr参数当作存放映射区的一个建议,不保证请求的地址会使用。而使用此标志后强制使用addr参数指定的地址,但此地址一定要是对齐的,对大多数架构,页的倍数的地址一定是对齐的,但有些架构会有额外的限制。将addr参数指定为0可获得最大的可移植性。此参数是POSIX系统的可选项,是XSI系统的必须项。
2.MAP_SHARED:修改映射区内容时也会修改磁盘文件内容,修改映射区使其他映射了该文件的进程也可见。此标志和MAP_PRIVATE标志必须指定一个,但不能同时指定。
3.MAP_PRIVATE:对映射区的修改会导致创建该映射文件的一个私有副本,后续所有对该映射区的引用都是引用该副本。(一种用途是调试程序,将程序文件的正文映射到存储区,用户的修改只影响程序文件的副本,而不影响源文件)。
4.其他实现特有的标志。

如果指定了MAP_FIXED标志,通常要求off和addr参数的值是系统虚拟存储页长度的倍数,虚拟存储页长度可用_SC_PAGESIZE或_SC_PAGE_SIZE为参数调用sysconf函数得到。该限制是操作系统实现强加的,SUS没有要求满足该条件。通常这两个参数都会设为0。

如果映射区的长度不是虚拟存储页长的整数倍时,如文件长12字节,系统页长512字节,则系统通常提供512字节的映射区,其中的后500字节被设为0,后面的500字节可被修改,但修改不会保存到磁盘文件,如想用mmap函数在文件尾后增加数据,需要先增加文件大小,方法是将文件用ftruncate函数截断到指定大小,可以将文件截断到文件尾后,表示扩大文件到指定位置。

与映射区有关的信号有SIGSEGV和SIGBUS。SIGSEGV通常用于指示进程试图访问对它不可用的存储区,如果存储区是只读的,进程在写该存储区时也会产生此信号;如果映射区的某个部分在访问时其对应的文件部分已不存在,则产生SIGBUS信号,如用文件长度映射了一个文件,但该文件之后被其他进程截断,然后我们访问映射区会收到SIGBUS信号。

子进程能通过fork函数继承映射区,因为子进程复制父进程地址空间。新程序不能通过exec函数继承映射区。

更改现有映射区的访问权限:
在这里插入图片描述
prot参数与mmap函数的prot参数一样。地址参数addr的值必须是系统页长的整数倍。

如果将映射区的访问权限改为MAP_SHARED,则映射区的修改不会立即写回文件中,何时写回脏页由内核的守护进程决定,具体取决于系统负载和用来限制系统在崩溃时数据损失的配置参数。一个页中只要修改了一个字节,整个页都要写回文件。

将MAP_SHARED映射区中的被修改的页冲洗到磁盘文件中,类似于fsync函数:
在这里插入图片描述
如果映射区是MAP_PRIVATE的,则以上函数不会修改磁盘文件。addr参数需要与页边界对齐。

flags参数可选值:
1.MS_ASYNC:将页简单地安排将被写入就返回。
2.MS_SYNC:将页写入磁盘后再返回,1和2必须指定一个。
3.MS_INVALIDATE:丢弃脏页。一些实现会丢弃参数指定范围内的所有页,这种行为不是必需的。

msync函数包含在SUS的XSI选项中,所有UNIX系统都支持它。

进程终止时,会自动解除映射区的映射,关闭映射文件映射时使用的文件描述符不会解除映射区映射。用以下函数也可以解除映射区:
在这里插入图片描述
调用munmap不会使映射区内容写到磁盘,MAP_SHARED映射区中修改的内容会在修改后按内核的虚拟内存算法自动保存到磁盘文件上。存储区解除映射后,MAP_PRIVATE映射区的修改会被直接丢弃。

addr参数必须是页大小的整数倍。后续对已解除映射的内存访问会产生SIGSEGV。

用存储映射IO复制文件:

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>

#define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
#define COPYINCR (1024*1024*1024)    // 1 GB

int main(int argc, char *argv[]) {
    int fdin, fdout;
    void *src, *dst;
    size_t copysz;
    struct stat sbuf;
    off_t fsz = 0;

    if (argc != 3) {
        printf("usage: %s <fromfile> <tofile>\n", argv[0]);
		exit(1);
    }

    if ((fdin = open(argv[1], O_RDONLY)) < 0) {
        printf("Can't open %s for reading\n", argv[1]);
		exit(1);
    }

    if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0) {
        printf("can't creat %s for writing\n", argv[2]);
		exit(1);
    }

    if (fstat(fdin, &sbuf) < 0) {    // need size of input file
        printf("fstat error\n");
		exit(1);
    }

    if (ftruncate(fdout, sbuf.st_size) < 0) {    // set outpur file size
        printf("ftruncate error\n");
		exit(1);
    }

    while (fsz < sbuf.st_size) {
        if ((sbuf.st_size - fsz) > COPYINCR) {
		    copysz = COPYINCR;
		} else {
		    copysz = sbuf.st_size - fsz;
		}
	
		if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED) {
	        printf("mmap error for input\n");
		    exit(1);
		}
		if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, fsz)) == MAP_FAILED) {
		    printf("mmap error for output\n");
		    exit(1);
		}
	
		memcpy(dst, src, copysz);    // does the file copy
		munmap(src, copysz);
		munmap(dst, copysz);
		fsz += copysz;
    }
    exit(0);
}

如果没有调用ftruncate扩大输出文件大小,mmap函数会调用成功,但memcpy函数访问相关存储区时会产生SIGBUS信号。

映射区中的内容被写道文件的确切时间依赖于系统的页管理算法,某些系统设置了守护进程,它将脏页随时间推移慢慢写到磁盘上,如想确保数据安全地写到了文件中,需要在进程终止前以MS_SYNC调用msync。

存储区映射复制与read、write函数复制一个300MB的文件时间比较,单位为秒:
在这里插入图片描述
注意以上程序中并没有退出前将数据同步到磁盘。

上图两系统上,两种方法的总CPU时间(用户时间+系统时间)几乎相同。两种方法的主要区别是read和write函数执行了更多系统调用,并做了更多复制,read和write函数会将数据从应用缓冲区和内核缓冲区间复制;mmap和memcpy函数直接将数据从一个内核缓冲区复制到另一个内核缓冲区,在复制时,如果引用了已经不存在的页,会发生页错误。系统调用和额外复制的开销相比页错误的开销不同时,一种方法就会优于另一种方法。

上图两系统上两种方法花费的总CPU时间差距不大,但时钟时间相差很大,原因可能是一种方法需要较长时间来等待IO完成,这个等待时间没有计算在CPU的处理时间中;还可能是一些系统处理花费的时间没有计算在程序的时间中,如系统守护进程将页写到磁盘的操作,由于需要为读和写分配页,系统守护进程会为我们准备可用的页(如果写是随机的而非连续的,将页写入磁盘所需时间会更长,页可被用来复用前等待的时间也更长)。

上例程序是否会改变被复制文件的访问时间取决于操作系统和文件系统。

在某些系统上,存储映射IO可能复制普通文件更快,但存储映射IO有限制,如不能用在某些特定设备间(如某个网络设备或某个终端设备)进行复制,还要注意磁盘文件的大小可能会在映射后发生改变。但有些程序还是可以从内存映射IO中获得好处,因为可用内存操作简化write、read操作。

当一个进程对某文件加写锁时被阻塞,而另一个进程又对文件发出加读锁请求,测试加写锁的进程是否会饿死:

#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

typedef void Sigfunc(int);

void sigint(int signo) { }

Sigfunc *signal_intr(int signo, Sigfunc *func) {
    struct sigaction act, oact;
    
    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
#ifdef SA_INTERRUPT
    act.sa_flags |= SA_INTERRUPT;    // 中断系统调用后不再重新调用
#endif
    if (sigaction(signo, &act, &oact) < 0) {
        return SIG_ERR;
    }
    return oact.sa_handler;
}

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len) {
    struct flock lock;

    lock.l_type = type;
    lock.l_start = offset;
    lock.l_whence = whence;
    lock.l_len = len;

    return fcntl(fd, cmd, &lock);
}

int main(void) {
    pid_t pid1, pid2, pid3;
    int fd;

    setbuf(stdout, NULL);
    signal_intr(SIGINT, sigint);

    // create a file
    if ((fd = open("lockfile", O_RDWR | O_CREAT, 0666)) < 0) {
        printf("can't open/create lockfile\n");
		exit(1);
    }

    // read-lock the file
    if ((pid1 = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid1 == 0) {    // child
        if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET,0) < 0) {
		    printf("child 1: can't read-lock file\n");
		    exit(1);
		}
        printf("child 1: obtained read lock on file\n");
		pause();
		printf("child 1: exit after pause\n");
		exit(0);
    } else {    // parent
        sleep(2);
    }

    // parent continues ... read-lock thefile again
    if ((pid2 = fork()) < 0) {
        printf("fork failed\n");
		exit(1);
    } else if (pid2 == 0) {    // child
        if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET, 0) < 0) {
		    printf("child 2: can't read-lock file\n");
		    exit(1);
		}
		printf("child 2: obtained read lock on file\n");
		pause();
		printf("child 2: exit after pause\n");
		exit(0);
    } else {    // parent
        sleep(2);
    }

    // parent continues ... block while trying to write-lock the file
    if ((pid3 = fork()) < 0) {
        printf("fork failed\n");
		exit(1);
    } else if (pid3 == 0) {    // child
        if (lock_reg(fd, F_SETLK, F_WRLCK, 0, SEEK_SET, 0) < 0) {
		    printf("child 3: can't set write lock: %s\n", strerror(errno));
		}
		printf("child 3 about to block in write-lock...\n");
		if (lock_reg(fd, F_SETLKW, F_WRLCK, 0, SEEK_SET, 0) < 0) {    // 被信号中断时返回-1表示出错
		    printf("child 3: can't write-lock file\n");
		    exit(1);
		}
		printf("child 3 returned and got write lock???\n");
		pause();
		printf("child 3: exit after pause\n");
		exit(0);
    } else {    // parent
        sleep(2);
    }

    // see if a pending write lock will block the nect read-lock attempt
    if (lock_reg(fd, F_SETLK, F_RDLCK, 0, SEEK_SET, 0) < 0) {
        printf("parent: can't set read lock: %s\n", strerror(errno));
		exit(1);
    } else {
        printf("parent: obtained additional read lock while write lock is pending\n");
    }
    printf("killing child 1...\n");
    kill(pid1, SIGINT);
    printf("killing child 2...\n");
    kill(pid2, SIGINT);
    printf("killing child 3...\n");
    kill(pid3, SIGINT);
    sleep(3);
    exit(0);
}

运行它:
在这里插入图片描述
在FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8上,运行结果都是上图这样,后增加的读会使未决的写不断等待。

大多数系统将数据类型fd_set(select函数的4个FD_宏)定义为只包含一个成员的结构,该成员为长整型数组,数组中每一位对应一个描述符,4个FD_宏通过开、关或测试指定的位对这个数组进行操作。将其定义为结构体而非一个数组的原因是:通过C语言的赋值语句可使fd_set类型变量相互赋值。

fd_set类型有一个内置的最大描述符数量的限制,大多数系统允许用户在包括头文件sys/select.h前定义常量FD_SETSIZE来改变fd_set的最大描述符数量限制。但现代操作系统中做以上事前,还需做以下工作:
1.在引入任何头文件前,我们需要定义阻止我们引入头文件sys/select.h的符号(防止有些头文件中包含此头文件),一些系统可能用另一个符号来保护(保护的含义是防止引入两次而设置符号来保护,与阻止引入头文件的符号作用相同,我们需要在更改FD_SIZE后再定义fd_set)fd_set的定义。如在FreeBSD 8.0中,我们需要使用_SYS_SELECT_H_符号来阻止引入头文件sys/select.h,还需要使定义_FD_SET符号来阻止引入fd_set类型的定义。
2.有时,为了老版本兼容性,sys/types.h头文件中定义了fd_set的大小,因此我们需要先包含此头文件,然后取消FD_SETSIZE的定义。有些系统使用__FD_SETSIZE来取代FD_SETSIZE。
3.重新定义FD_SETSIZE或__FD_SETSIZE为最大文件描述符数,之后select函数可以支持这么多的文件描述符。
4.取消步骤1中的符号定义。
5.引入头文件sys/select.h。

对比对fd_set的操作和对信号集的操作的函数:
在这里插入图片描述
sigfillset函数没有对应的FD_XXX函数。

用select函数实现sleep函数,但参数单位是微秒:

#include <sys/select.h>
#include <stddef.h>

void sleep_us(unsigned int nusecs) {
    struct timeval tval;
    
    tval.tv_sec = nusecs / 1000000;
    tval.tv_usec = nusecs % 1000000;
    select(0, NULL, NULL, NULL, &tval);
}

用poll函数实现sleep函数,但参数单位是微秒:

#include <poll.h>

void sleep_us(unsigned int nusecs) {
    struct pollfd dummy;
    int timeout;

    if ((timeout = nusecs / 1000) <= 0) {    // 如果要等待的时间为负,等待1毫秒
        timeout = 1;
    }
    poll(&dummy, 0, timeout);    // timeout小于0时永久等待
}

BSD的usleep函数使用nanosleep函数,该函数没有与调用进程设置的定时器交互。

记录锁不能用于实现TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT、WAIT_CHILD函数,如果我们用TELL_WAIT创建一个临时文件,其中1字节用作父进程的锁,另一个字节用作子进程的锁,WAIT_CHILD使父进程等待获取子进程字节上的锁,TELL_PARENT使子进程释放子进程字节上的锁,问题在于调用fork后会释放子进程中的锁,使得子进程开始运行时不具备任何锁。

用非阻塞写测试管道的容量:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

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);
    }
}

int main() {
    int i, n;
    int fd[2];

    if (pipe(fd) < 0) {
        printf("pipe error\n");
        exit(1);
    }
    set_fl(fd[1], O_NONBLOCK);

    // write 1 byte at a time until pipe is full
    for (n = 0; ; ++n) {
        if ((i = write(fd[1], "a", 1)) != 1) {
		    printf("write ret %d ", i);
		    break;
		}
    }

    printf("pipe capacity = %d\n", n);
}

最终得出的管道大小依赖于系统实现,并且值可能不等于PIPE_BUF,因为PIPE_BUF的含义是可原子地写入管道中的最大数据量,而以上程序获得的是摆脱了原子性的一个管道的最大容量。

调用mmap成功后,就可以关闭文件了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值