1. 非阻塞I/O
系统调用分为两类:低速系统调用和其他系统调用。
低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:
- 如果某些文件类型(如读管道、终端设备和网络设备)的数据并不存在,读操作可能使调用者永远阻塞。
- 如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用者永远阻塞。
- 在某种条件发生之前打开某些文件类型可能会发生阻塞(例如以只写模式打开FIFO,那么在没有其他进程用读模式打开该FIFO时也要等待)
- 对已经加上强制性记录锁的文件进行读写
- 某些ioctl操作
- 某些进程间通信函数
1.1 非阻塞I/O:
非阻塞I/O使我们可以发出open、read和write这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如果继续进行将阻塞。
对于一个非阻塞的描述符如果无数据可读,则read返回-1,errno为EAGAIN。
非阻塞I/O指的是文件状态标志,即与文件表项有关。会影响使用同一文件表项的所有文件描述符(即使属于不同的进程)。
注意,read函数阻塞的情况:
read函数只是一个通用的读文件设备的接口。是否阻塞需要由设备的属性和设定所决定。一般来说,读字符终端、网络的socket描述字,管道文件等,这些文件的缺省read都是阻塞的方式。如果是读磁盘上的文件,一般不会是阻塞方式的。但使用锁和fcntl设置取消文件O_NOBLOCK状态,也会产生阻塞的read效果。
1.2 对于一个给定的文件描述符,有两种方式为其指定非阻塞I/O:
-
如果用open获得描述符,指定O_NONBLOCK标志
-
对于一个已经打开的文件描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志
int flag = fcntl(fd, F_GETFL); //获取文件状态标志 flag |= O_NONBLOCK; int ret = fcntl(fd, F_SETFL, flag); //设置文件状态标志
2. 记录锁 record locking
在大多数UNIX系统中,当两个人同时编辑一个文件时,该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序如数据库,进程有时需要确保它正在单独写一个文件。因此可以使用记录锁机制。
记录锁的功能:当第一个进程正在读或者修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。
其实应该称记录锁为**“字节范围锁”**,因为它锁定的只是文件中的一个区域(也可能是整个文件)
2.1 fcntl记录锁
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. */
};
- 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字节),和开始时又一样了。
2.2 锁的隐含继承和释放
-
当一个进程终止时,它所建立的锁全部释放;无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。
比如fd1代表一个打开文件,调用dup后fd2也指向该文件。那么当close(fd2)后,fd1和fd2指向的文件上设置的锁都被释放。
-
由fork产生的子进程不继承父进程所设置的锁。因为对于父进程获得的锁而言,子进程被视为另一个进程。
-
执行exec后,新程序可以继承原执行程序的锁。但是如果该文件描述符设置了close-on-exec标志,则exec之后释放相应文件的锁
示例
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();
其结果如下:
可以看出来,文件记录锁信息是保存在文件v节点/inode节点上的(而不是在文件表项中的),其实现是通过一个链表记录该文件上的各个锁,因此能保证多个进程正确操作文件记录锁。
在父进程中,关闭fd1、fd2、fd3中的任意一个都将释放由父进程设置的写锁。内核会从该描述符锁关联的inode节点开始,逐个检查lockf链表中的各项,并释放由调用进程持有的各把锁。
在十三章中,我们知道守护进程可用一把文件锁来保证只有该守护进程的唯一副本在运行,其lockfile函数实现如下:守护进程可用该函数在文件上加独占写锁
int lockfile(int fd)