非阻塞IO与记录锁

2015-02-15 20:31:06   最后更新: 2015-02-16 14:38:21   访问数量:1396




概述

我们可以将系统调用分成低速系统调用和其他系统调用两类

低速系统调用有可能会使进程永远阻塞,低速系统调用包括下列情况:

  • 如果某些文件类型的数据不存在,则读操作的调用者可能永远阻塞
  • 如果数据不能立即被同样类型的文件接受(如管道中无空间、网络流控制等),则写操作的调用者可能永远阻塞
  • 在某种条件发生之前,打开某些类型的文件会被阻塞(如打开一个终端设备可能需要等到与之连接的调制解调器应答,又例如在没有其他进程已用读模式打开该FIFO时若以致谢模式打开FIFO,则也需要等待)
  • 对已经加上强制性记录锁的文件进行读写
  • 某些 ioctl 操作
  • 某些进程间通信函数

虽然读写磁盘文件会使调用者在短时间内被阻塞,但是并不能将与磁盘IO相关的系统调用视为“低速”系统调用

 

非阻塞调用

  • 对于一个给定描述符,有两种方法可以指定为非阻塞IO

  1. 如果调用 open 获得描述符,则可制定 O_NONBLOCK 标志
  2. 对于已经打开的一个描述符,则可调用 fcntl 打开 O_NONBLOCK 标志

 

示例

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> void set_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) { printf("fcntl F_GETFL error"); exit(-1); } val |= flags; if (fcntl(fd, F_SETFL, val) < 0) { printf("fcntl F_SETFL error"); exit(-1); } } void clr_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) { printf("fcntl F_GETFL error"); exit(-1); } val &= ~flags; if (fcntl(fd, F_SETFL, val) < 0) { printf("fcntl F_SETFL error"); exit(-1); } } char buf[500000]; 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, 500); fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno); if (nwrite > 0) { ptr += nwrite; ntowrite -= nwrite; } } clr_fl(STDOUT_FILENO, O_NONBLOCK); return 0; }

 

通过执行:

yes | awk "{print 'abcdefghijklmnopqrst'}" | head -100000 > stdin.txt

 

获得了一个 26*100,000 个字符的文件作为输入文件

执行:

./main<stdin.txt 2>stderr.log

 

执行后结果如下:

 

可见,write 系统调用出现了大量 errno 为 11 的 error

errno 值 11 对应的错误是 EAGAIN

在这一简单的程序执行过程中,1001 次成功执行的结果中,却出现了 800787 次失败的出错返回,这种形式的循环被称为“轮询”

正如例子中展示的,在多用户系统上,他浪费了大量的CPU时间,因此这种操作是不建议使用的,取而代之,用户可以选择使用 IO 多路转接:

IO复用:select 函数和 poll 函数

也可以使用多线程的策略来避免使用非阻塞IO

 

概述

若两个人同时写入一个文件,进程将无法确定文件的最终状态

很多应用程序需要确保同时只有一个进程在编辑该文件,因此 UNIX 提供了记录锁机制

当一个进程正在读或写一个文件,他可以阻止其他进程修改同一文件区(一段字节范围或整个文件)

 

fcntl 记录锁

int fcntl(int fileds, int cmd, ... /* stuct flock *flockptr */);

 

定义于 fcntl.h 中

调用成功的返回依赖于 cmd,出错返回 -1

 

参数说明

  • cmd

对于记录锁 cmd 取值为 F_GETLK、F_SETLK 或 F_SETLKW

记录锁 cmd 参数取值
取值说明
F_GETLK判断 flockptr 所描述区域是否有一把锁所阻塞,如果存在则将信息通过 flockptr 参数返回,否则将 l_type 置为 F_UNLCK,其他信息保持不变
F_SETLK设置由 flockptr 所描述的锁,如不被允许则立即返回 -1,同时 errno 被设置为 EACCES 或 EAGAIN,如果 l_type 为 F_UNLCK,则清除 flockptr 所说明的锁
F_SETLKWF_SETLK 的阻塞版本,即如果设置不被允许,则调用进程休眠,直到请求创建的所已经可用或被信号唤醒

 

  • flockptr

flockptr 取值为一个指向 flock 结构的指针

struct flock { short l_type; /* F_RDLCK, F_WRLCK or F_UNLCK */ off_t l_start; /* offset in bytes */ short l_whence; /* SEEK_SET, SEEK_CUR or SEEK_END */ off_t l_len; /* length in bytes, 0 means lock to EOF */ pid_t l_pid; /* returned with F_GETLK */ }

 

 

flock 结构字段说明
字段说明
l_type锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁某区域)
l_start加锁或解锁区域的起始偏移
l_whence决定了相对偏移量的起点,可选 SEEK_SET、SEEK_CUR 或 SEEK_END
l_len确定锁定的字节数,若为 0 则锁定区域从起点开始直到 EOF
l_pid当 fcntl 函数的 cmd 参数为 F_GETLK 时,该结构作为值-结果参数,同时 l_pid 作为返回值,即该锁定区域的锁定进程的 PID

 

l_start 与 l_whence 与 lseek 函数的最后两个参数非常类似,可以参看:

lseek函数

 

与线程的读写锁相同,当文件某区域设置了读锁,则该区域允许一个或多个进程读,但是不允许任何进程写入数据,而如果某区域设置了写锁,则该区域拒绝任何读写操作

 

示例

#include <fcntl.h> #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_SETLKW, F_UNLCK, (offset), (whence), (len)) #define read_lockable(fd, offset, whence, len) \ (lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0) #define write_lockable(fd, offset, whence, len) \ (lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0) #define lock_file(fd) \ write_lock((fd), 0, SEEK_SET, 0) 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 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; return lock.l_pid; }

 

 

上述代码定义了 8 个宏函数,分别用来实现阻塞/非阻塞的读/写锁、测试读/写锁、文件锁

  • 需要注意的是,对一个文件锁两次即会造成死锁,而如果一个文件对他已经加过锁的文件进行测试,则一定会返回可以加锁

 

锁的隐含继承和释放

关于记录锁的自动继承和释放遵循下列三条规则:

  1. 锁与进程和文件两方面有关:当一个进程终止时,他所建立的所有锁将全部释放,而关闭一个描述符时,则该进程通过这一描述符所引用的文件上的所有的锁都将被释放(通过 dup 复制的描述符上设置的锁也将全部被释放)
  2. 由 fork 产生的子进程不继承父进程锁设置的锁
  3. 在执行 exec 后,新程序可以继承原执行程序的锁(如果文件描述符已经设置了 close-on-exec 标志,则相应文件上的所有锁均会被释放)

 

在文件尾端加锁

writew_lock(fd, 0, SEEK_END, 0); write(fd, buf, 1); un_lock(fd, 0, SEEK_END, 0); write(fd, buf, 1);

 

上述代码做了什么呢?

首先,在文件尾端加了一把写锁,这样的锁包括以后可能添加到该文件的任何数据,这之后,写入一个字符后,该字符也随即被加上写锁

随后,进行了一次从尾端开始的解锁操作,从此以后新添加的字符都不会被加锁

但是,需要注意的是,此前锁住的 1 个字符,此时已然处于写锁定的状态

 

unix 与记录锁

当一些 unix 命令执行时,他们会对文件加锁,具体会出现下列情况:

  • 使用 ed 编辑程序可以对已加锁文件进行编辑,因为 ed 的实际操作是写入临时文件,在保存时,用临时文件替换原文件,因此锁对 ed 命令毫无影响,同样的,锁也不会对 unlink 调用产生任何影响
  • 不能使用 vi 编辑程序编辑已加锁文件,可读,但是不可写入

 






读书笔记      操作系统      linux      unix      c语言      龙潭书斋      apue      io            死锁      记录锁     


京ICP备15018585号