首页 > 代码库 > 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的一部分关闭该文件描述符时,对相应文件的所有锁都被释放。