少儿编程培训机构,佛山公司推广优化,北京市建设工程信息网知名中项网,建设部证书查询官方网站1、引言
2、 非阻塞I/O
系统调用分为两类#xff1a;低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用#xff0c;包括#xff1a; 如果某些文件类型#xff08;如读管道、终端设备和网络设备#xff09;的数据并不存在#xff0c;读操作…1、引言
2、 非阻塞I/O
系统调用分为两类低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用包括 如果某些文件类型如读管道、终端设备和网络设备的数据并不存在读操作可能使调用者永远阻塞。如果数据不能被相同的文件类型立即接受如管道中无空间、网络流控制写操作可能会使调用者永远阻塞。在某种条件发生之前打开某些文件类型可能会发生阻塞例如以只写模式打开FIFO那么在没有其他进程用读模式打开该FIFO时也要等待对已经加上强制性记录锁的文件进行读写某些ioctl操作某些进程间通信函数 非阻塞I/O使我们可以发出open、read和write这样的I/O操作并使这些操作不会永远阻塞。如果这种操作不能完成则调用立即出错返回表示该操作如果继续进行将阻塞。对于一个非阻塞的描述符如果无数据可读则read返回-1errno为EAGAIN。非阻塞I/O指的是文件状态标志即与文件表项有关。会影响使用同一文件表项的所有文件描述符即使属于不同的进程。注意read函数阻塞的情况read函数只是一个通用的读文件设备的接口。是否阻塞需要由设备的属性和设定所决定。一般来说读字符终端、网络的socket描述字管道文件等这些文件的缺省read都是阻塞的方式。如果是读磁盘上的文件一般不会是阻塞方式的。但使用锁和fcntl设置取消文件O_NOBLOCK状态也会产生阻塞的read效果。对于一个给定的文件描述符有两种方式为其指定非阻塞I/O 如果用open获得描述符指定O_NONBLOCK标志对于一个已经打开的文件描述符则可调用fcntl由该函数打开O_NONBLOCK文件状态标志int flag fcntl(fd, F_GETFL); //获取文件状态标志
flag | O_NONBLOCK;
int ret fcntl(fd, F_SETFL, flag); //设置文件状态标志实例 一个非阻塞I/O的实例。它从标准输入读50000个字节并试图将它们写到标准输出上。该程序先将标准输出设置为非阻塞的然后用for循环进行输出每次write调用的结果都在标准错误上打印。其中set_fl函数的介绍见3.14节get_fl函数则类似于set_fl函数。#include apue.h
#include errno.h
#include fcntl.hchar buf[500000];int
main(void)
{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); /* set nonblocking */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); /* clear nonblocking */exit(0);
}若标准输出是普通文件则write一般只调用一次。这里的文件/etc/services大小为19605字节小于程序中的50000字节所以写入的大小也为19605字节。如果文件大小大于50000字节那么写入的大小为50000字节。 lhLH_LINUX:~/桌面/apue.3e/advio$ ls -l /etc/services
-rw-r--r-- 1 root root 19605 10月 25 2014 /etc/services
lhLH_LINUX:~/桌面/apue.3e/advio$ ./nonblockw /etc/services temp.file
read 19605 bytes
nwrite 19605, errno 0
lhLH_LINUX:~/桌面/apue.3e/advio$ ls -l temp.file
-rw-rw-r-- 1 lh lh 19605 8月 9 13:15 temp.file若标准输出是终端则有时返回数字有时返回错误下面是运行结果。 该系统上errno的值35对应的是EAGAIN。终端驱动程序一次能接受的数据量随系统而变。在此实例中程序发出了9000多个write调用但是只有500个真正输出了数据其余的都只返回了错误。这种形式的循环称为轮询在多用户系统上用它会浪费CPU时间。14.4节会介绍非阻塞描述符的I/O多路转换这是进行这种操作的一种比较有效的方法。有时可以将应用程序设计成多线程的从而避免使用非阻塞I/O。如若我们能在其他线程中继续进行则可以允许单个线程在I/O调用中阻塞。但线程间的同步的开销有时却可能增加复杂性于是导致得不偿失的后果。
3、 记录锁
当两个人同时编辑一个文件时该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序如数据库进程有时需要确保它正在单独写一个文件。因此可以使用记录锁机制。记录锁的功能当第一个进程正在读或者修改文件的某个部分时使用记录锁可以阻止其他进程修改同一文件区。其实应该称记录锁为 “字节范围锁” 因为它锁定的只是文件中的一个区域也可能是整个文件
3.1、历史
这部分的内容不重要略过。
3.2、fcntl记录锁 3.14节已经给出了该函数的原型 int fcntl(int fd, int cmd, ... /* struct flock * flockptr */ );与记录锁相关的cmd是F_GETLK、F_SETLK、F_SETLKW。 F_GETLK 判断由flockptr描述的锁是否会被另外一把锁排斥阻塞。如果存在一把锁阻止创建flockptr描述的锁则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况除了l_type设置为F_UNLCK之外flockptr指向的结构中其他信息不变。注意由于调用进程自己的锁并不会阻塞自己的下一次尝试加锁因为新锁将替换旧锁因此F_GETLK不会报告调用进程自己持有的锁信息。因此不能用它来测试自己是否在某一文件区域持有一把锁。 F_SETLK 设置由flockptr所描述的锁共享读锁或独占写锁。如果失败fcntl函数立即出错返回errno设置为EACCES或EAGAGIN F_SETLKW 这个命令是F_SETLK的阻塞版本w表示等待wait。如果所请求的读锁或写锁因另一个进程当前已经对所请求部分进行了加锁而不能被授予那么调用进程休眠。如果请求创建的锁已经可用或者休眠被信号中断则该进程被唤醒。 第三个参数是一个指向flock结构的指针。struct flock{short int l_type; /* 记录锁类型: F_RDLCK, F_WRLCK, or F_UNLCK. */short int l_whence; /* SEEK_SET、SEEK_CUR、SEEK_END */__off_t l_start; /* Offset where the lock begins. */__off_t l_len; /* Size of the locked area; zero means until EOF. */__pid_t l_pid; /* Process holding the lock. */};对flock结构说明如下 l_type所希望的锁类型。F_RDLCK共享读锁、F_WRLCK独占性写锁、F_UNLCK解锁一个区域l_whence指示l_start从哪里开始。SEEK_SET开头、SEEK_CUR当前位置、SEEK_END结尾l_start要加锁或解锁区域的起始字节偏移量l_len要加锁或解锁区域字节长度l_pid仅由F_GETLK返回表示该pid进程持有的锁能阻塞当前进程。 关于加锁和解锁区域的说明还要注意以下事项 锁可以在当前文件尾端处开始或者越过尾端处开始但是不能在文件起始位置之前开始。如果l_len为0则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向该文件中追加写了多少数据它们都可以处于锁的范围内不必猜测会有多少字节被追加写到了文件之后为了对整个文件加锁设置l_start和l_whence指向文件起始位置并且指定长度l_len为0。 fcntl可以操作两种锁共享读锁F_RDLCK和独占性写锁F_WRLCK 任意多个进程在一个给定的字节上可以有一把共享的读锁但是在一个给定字节上只能有一个进程有一把独占写锁。如果在一个给定字节上已经有一把或多把读锁则不能在该字节上再加写锁如果在一个字节上已经有一把独占性写锁则不能再对它加任何读锁。如果一个进程对一个文件区间已经有了一把锁后来该进程又企图在同一文件区间再加一把锁那么新锁将替换已有锁。比如一个进程在某文件的16-32字节区间有一把写锁然后又试图在16-32字节区间加一把读锁那么该请求成功执行原来的写锁替换为读锁。加读锁时描述符必须是读打开加写锁时描述符必须是写打开。 需要注意以下两点 用F_GETLK测试能否建立一把锁然后用F_SETLK或F_SETLKW企图建立那把锁这两者不是一个原子操作。不能保证两次fcntl调用之间不会有另一个进程插入并建立一把锁POSIX没有说明下列情况会发生什么 第一个进程在某文件区间设置一把读锁第二个进程试图在同一文件区间加一把写锁时阻塞然后第三个进程则试图在同一文件区间设置另一把读锁。如果允许第三个进程获得读锁那么这种实现容易导致希望加写锁的进程饿死。 文件记录锁的组合和分裂 在设置或释放文件上的一把锁时系统按照要求组合或分裂相邻区。 例如在100-199字节是加锁区域当需要解锁第150字节时则内核将维持两把锁一把用于100-149字节另一把用于151-199字节。如果我们又对第150字节加锁那么系统会把相邻的加锁区合并成一个区100-199字节和开始时又一样了。 实例请求和释放一把锁。为了每次都避免分配flock结构然后又填入各项信息可以用下图程序中的lock_reg来处理这些细节。 因为大多数锁调用时加锁或解锁一个文件区域命令F_GETLK很少使用故通常使用下列5个宏中的一个。这5个宏都定义在apue.h中见附录B 实例测试一把锁。如果存在一把锁它阻塞由参数指定的锁请求则此函数返回持有这把现有锁的进程的进程 ID否则此函数返回0。 通过用下面两个宏来调用此函数它们也定义在apue.h中 注意进程不能使用lock_test函数测试它自己是否在文件的某一部分持有一把锁。F_GETLK命令的定义说明返回信息指示是否有现有的锁阻止调用进程设置它自己的锁。因为F_SETLK和F_SETLKW命令总是替换调用进程现有的锁若已存在所以调用进程绝不会阻塞在自己持有的锁上。于是F_GETLK命令绝不会报告调用进程自己持有的锁。 实例死锁。该例中子进程对第0字节加锁父进程对第1字节加锁。然后它们中的每一个又试图对对方已经加锁的字节加锁。 程序中介绍了8.9节中介绍的父进程和子进程同步例程。 #include apue.h
#include fcntl.hstatic void
lockabyte(const char *name, int fd, off_t offset)
{if (writew_lock(fd, offset, SEEK_SET, 1) 0)err_sys(%s: writew_lock error, name);printf(%s: got the lock, byte %lld\n, name, (long long)offset);
}int
main(void)
{int fd;pid_t pid;/** Create a file and write two bytes to it.*/if ((fd creat(templock, FILE_MODE)) 0)err_sys(creat error);if (write(fd, ab, 2) ! 2)err_sys(write error);TELL_WAIT(); /*set things up for TELL_xxx WAIT_xxx */if ((pid fork()) 0) {err_sys(fork error);} else if (pid 0) { /* child */lockabyte(child, fd, 0);TELL_PARENT(getppid()); /*tell parent were done */WAIT_PARENT(); /*and wait for parent*/lockabyte(child, fd, 1);} else { /* parent */lockabyte(parent, fd, 1);TELL_CHILD(pid); /*tell child were done */WAIT_CHILD(); /*and wait for parent*/ lockabyte(parent, fd, 0);}exit(0);
}运行该实例可以得到 lhLH_LINUX:~/桌面/apue.3e/advio$ ./deadlock
parent: got the lock, byte 1
child: got the lock, byte 0
parent: writew_lock error: Resource deadlock avoided
child: got the lock, byte 1检测到死锁时内核必须选择一个进程接收出错返回。在本实例中选择了父进程。选择父进程还是子进程出错返回随操作系统而定。
3.3、锁的隐含继承和释放
关于记录锁的自动继承和释放有3条规则。 锁与进程和文件两者相关联当一个进程终止时它所建立的锁全部释放无论一个描述符何时关闭该进程通过这一描述符引用的文件上的任何一把锁都会释放这些锁都是该进程设置的。 例如在close(fd)后在fd1设置的锁被释放。dup函数的使用方法见3.12节fd1 open(pathname, ...);
read_lock(fd1, ...);
fd2 dup(fd1);
close(fd2);如果dup替换成open其效果也一样fd1 open(pathname, ...);
read_lock(fd1, ...);
fd2 open(fd1);
close(fd2);由fork产生的子进程不继承父进程所设置的锁。因为对于父进程获得的锁而言子进程被视为另一个进程。执行exec后新程序可以继承原执行程序的锁。但是如果该文件描述符设置了close-on-exec标志则exec之后释放相应文件的锁。
3.4、FreeBSD实现
考虑一个进程他执行下列语句忽略出错返回fd1 open(pathname,...);
write_lock(fd1,0,SEEK_SET,1); // 该函数是自定义的父进程在字节0上设置写锁
if((pid fork()) 0) { // 父进程fd2 dup(f1);fd3 open(pathname,...);
} else if(pid 0) { // 子进程read_lock(fd1,1,SEEK_SET,1); //该函数是自定义的子进程在字节1上设置读锁
}
pause();在父进程和子进程暂停执行pause()之后数据结构的情况如下 可以看出来文件记录锁信息是保存在文件v节点/inode节点上的而不是在文件表项中的其实现是通过一个链表记录该文件上的各个锁因此能保证多个进程正确操作文件记录锁。在图中显示了两个lockf结构一个是由父进程调用write_lock形成的另一个则是子进程调用read_lock形成的。每一个结构都包含了相应的进程ID。在父进程中关闭fd1、fd2、fd3中的任意一个都将释放由父进程设置的写锁。内核会从该描述符锁关联的inode节点开始逐个检查lockf链表中的各项并释放由调用进程持有的各把锁。 实例守护进程可用一把文件锁来保证只有该守护进程的唯一副本在运行其lockfile函数实现如下守护进程可用该函数在文件整体上加独占写锁。int lockfile(int fd) {struct flock fl;fl.l_type F_WRLCK;fl.l_start 0;fl.l_whence SEEK_SET;fl.l_len 0;return fcntl(fd,F_SETLK,fl);
}另一种方法是write_lock函数定义lockfile函数#define lockfile(fd) write_lock((fd),0,SEEK_SET,0)3.5、在文件的尾端加锁
在对相对于文件尾端的字节范围加锁解锁必须特别小心。如下面代码write_lock(fd,0,SEEK_END,0);
write(fd,buf,1);
un_lock(fd,0,SEEK_END);
write(fd,buf,1);刚开始获得一把写锁该锁从当前文件尾开始包括以后可能追加写到该文件的任何数据。当文件偏移量处于文件尾时write一个字节将文件延伸了一个字节因此该字节被加写锁。但是其后的解锁是对当前文件尾开始包括以后可能追加写到该文件的任何数据进行解锁因此刚才追加写入的一个字节保留加锁状态。之后又写入了一个字节由此代码造成的文件锁状态如图。