首页 > 代码库 > Linux环境编程之高级I/O(一):非阻塞I/O、记录锁
Linux环境编程之高级I/O(一):非阻塞I/O、记录锁
引言:高级I/O包括非阻塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll函数)、readv和writev函数以及存储映射I/O。
(一)非阻塞I/O
可能会使进程永远阻塞的一类系统调用有:
1、如果某些文件类型的数据并不存在,则读操作可能会使调用者永远阻塞。
2、如果数据不能立即被上述同样类型的文件接受,则写操作也会使调用者永远阻塞。
3、在某种条件发生之前,打开某些类型的文件会阻塞。
4、对已经加上强制记录锁的文件进行读、写。
5、某些ioctl操作。
6、某些进程间通信函数。
对于一个给定的描述符有两种方法对其指定非阻塞I/O:
1、如果调用open获得描述符,则可指定O_NONBLOCK标志。
2、对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。
/* *File Name : nonblock.c *Author : libing *Mail : libing1209@126.com */ #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> char buf[500000]; void set_fl(int fd, int flags) /*flags are file status falgs to turn on*/ { int val; if((val = fcntl(fd, F_GETFL, 0)) < 0) printf("fcntl F_GETFL error"); val |= flags; /*打开flags标志*/ if(fcntl(fd, F_SETFL, val) < 0) printf("fcntl F_SETFL error"); } void clr_fl(int fd, int flags) /*flags are file status falgs to turn on*/ { int val; if((val = fcntl(fd, F_GETFL, 0)) < 0) printf("fcntl F_GETFL error"); val &= ~flags; /*关闭flags标志*/ if(fcntl(fd, F_SETFL, val) < 0) printf("fcntl F_SETFL error"); } int main(void) { int errno; char *ptr; int ntowrite, nwrite; ntowrite = read(STDIN_FILENO, buf, sizeof(buf)); fprintf(stderr, "read %d bytes\n", ntowrite); set_fl(STDOUT_FILENO, O_NONBLOCK); /*设置非阻塞标志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); /*设置非阻塞标志O_NONBLOCK*/ exit(0); }缺点:对于非阻塞操作,一般形式为轮询,在多用户系统上它浪费了CPU时间。可以用select和poll解决CPU浪费的问题,后面会单独介绍。
(二)记录锁
记录锁的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock *flockptr*/);//返回值:若成功则依赖cmd,若出错则返回-1。
flockptr是一个指向flock结构的指针(bits/fcntl.h文件中)。
struct flock { short int l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */ short int l_whence; /* Where `l_start‘ is relative to (like `lseek‘). */ #ifndef __USE_FILE_OFFSET64 __off_t l_start; /* Offset where the lock begins. */ __off_t l_len; /* Size of the locked area; zero means until EOF. */ #else __off64_t l_start; /* Offset where the lock begins. */ __off64_t l_len; /* Size of the locked area; zero means until EOF. */ #endif __pid_t l_pid; /* Process holding the lock. */ };
对flock结构说明如下:
1、所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域)。
2、要加锁或解锁区域的起始字节偏移量,这由l_start和l_whence两者决定。
3、区域的字节长度,由l_len表示。
4、具有能阻塞当前进程的锁,其持有进程的ID存放在l_pid中。
关于加锁和解锁的区域的说明还要注意:
1、l_start是相对偏移量(字节),l_whence则决定了相对偏移量的起点。这与lseek函数中最后两个参数类似。确实,l_whence可选用的值是SEEK_SET、SEEK_CUR和SEEK_END。
2、该区域可以在当前文件尾端处开始或越过其尾端处开始,但不能再文件起始位置之前开始。
3、如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能偏移量为止,也就是不管添写到该文件中多少数据,它们都处于锁的范围内。
4、为了锁整个文件,设置l_start和l_whence,使锁的起点在文件起始处,并且说明长度(l_len)为0。
所得类型有两种:共享读锁(l_type为F_RDLCK)和独占写锁(F_WRLCK)。基本规则:多个进程在一个给定的字节上可以有一把共享读锁,但是在一个给定字节上只能有一个进程独用的一把写锁。如果在一个给定字节上已经有一把或多把读锁,则不能再该字节上再加写锁;如果在一个字节上已经有了一把独占性的写锁,则不能再对它加任何读锁。
上面所说的规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同以文件区间再加一把锁,那么新锁将替换老锁。
加读锁时,该描述符必须是读打开的;加写锁时,该描述符必须是写打开的。
根据cmd的不同,fcntl函数的三种命令如下:
1、F_SETLK 设置由flockptr所描述的锁。如果试图建立一把读锁(l_type设为F_RDLCK)或写锁(l_type设为F_WRLCK),如果失败,则fcntl立即出错返回,此时errno设置为EACCESS 或EAGAIN。此命令也用来清除由flockptr说明的锁(l_type为F_UNLCK)。
示例代码:
#define read_lock(fd, offset, whecne, len) lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len)) #define write_lock(fd, offset, whecne, len) lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len)) #define un_lock(fd, offset, whecne, len) lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len)) int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len) { struct flock lock; lock.l_type = type; /*F_RDLCK, F_WRLCK, F_UNLCK*/ lock.l_start = offset; /*byte offset, relative to l_whence*/ lock.l_whence = whence; /*SEEK_SET, SEEK_CUR, SEEK_END*/ lock.l_len = len; return(fcntl(fd, cmd, &lock)); }2、F_SETLKW 这是F_SETLK的阻塞版本。如果因为当前在所请求区间的某个部分另一个进程已经有一把锁,则由flockptr所请求的锁不能被创建,则使调用进程休眠。如果请求创建的锁可用,或者休眠由信号中断,则该进程被唤醒。
3、F_GETLK 判断由flockptr所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
#define is_read_lockable(fd, offset, whecne, len) lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len)) #define is_write_lockable(fd, offset, whecne, len) lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len)) int lock_test(int fd, int type, off_t offset, int whence, off_t len) { struct flock lock; lock.l_type = type; /*F_RDLCK, F_WRLCK*/ lock.l_start = offset; /*byte offset, relative to l_whence*/ lock.l_whence = whence; /*SEEK_SET, SEEK_CUR, SEEK_END*/ lock.l_len = len; if(fcntl(fd, F_GETLK, &lock) < 0) printf("fcntl error"); if(lock.l_type == F_UNLCK) return 0; /*false , region isn‘t locked by another proc*/ return(lock.l_pid); /*true , return pid of lock owner*/ }用F_GETLK测试能否建立一把锁,然后用F_SETLK和F_SETLKW企图建立一把锁,这两者不是一个原子操作。因此不能保证在这两次fcntl调用之间不会有另一个进程插入并建立一把相关的锁,从而使原来测试到的情况发生变化。如果不希望在建立锁时可能产生的长期阻塞,则应使用F_SETLK,并对返回结果进行测试,以判别是否成功地建立了所要求的锁。
死锁:如果两个进程相互等待对方持有并且锁定的资源时,则这两个进程就处于死锁状态。
apue上的死锁实例:
#include <fcntl.h> static void lockabyte(const char *name, int fd, off_t offset) { if(write_lock(fd, offset, SEEK_SET, 1) < 0) err_sys("%s:writew_lock error", name); printf("%s:got the lock,byte %ld\n",name , 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 errror"); if(write(fd, "ab", 2) != 2) err_sys("write error"); TELL_WAIT(); if((pid = fork()) < 0){ err_sys("fork error"); }else if(pid == 0){ lockabyte("child", fd, 0); TELL_PARENT(getppid()); WAIT_PARENT(); lockabyte("child", fd, 1); }else{ lockabyte("parent", fd, 1); TELL_PARENT(pid); WAIT_PARENT(); lockabyte("parent", fd, 0); } exit(0); }锁的继承和释放规则:
1、锁与进程和文件有关。当一个进程终止时,它所建立的锁全部释放;任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一把锁都被释放。
2、由fork产生的子进程不继承父进程所设置的锁。
3、在执行exec后,新程序可以继承原执行程序的锁。但如果对一个文件描述符设置了clos_on_exec标志,那么当作为exec的一部分关闭该文件描述符时,对相应文件的所有锁都被释放。