第十三章 高级I/O
13.1 非阻塞I/O
阻塞其实就是进入了休眠状态,交出了 CPU 控制权。比如 wait()、pause()、sleep()等函数都会进入阻塞。
阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的 I/O 操作是非阻塞的。
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的。
但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作。
13.1.1 阻塞 I/O 与非阻塞 I/O 读文件
在调用 open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行。
对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的。
鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下。
13.1.2 阻塞 I/O 的优点与缺点
当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;
而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮询等待,直到数据可读,要么直接放弃!
所以阻塞式 I/O 挂起休眠会交出 CPU资源;而非阻塞式轮询会消耗 CPU资源。
键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。
标准输入、标准输出、标准错误这仨文件描述符是进程运行时就打开的。
阻塞式IO会导致线程内函数内其他程序不能运行。
非阻塞IO会由于轮询导致CPU占有率较高。
I/O多路复用可以解决阻塞式IO和非阻塞式IO存在的问题。
13.2 I/O多路复用
13.2.1 何为 I/O 多路复用
I/O 多路复用可以监视多个文件描述符,当某个文件描述符可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。
I/O 多路复用技术是为了解决并发式 I/O 场景下进程或线程阻塞的阻塞问题。
IO multiplexing,IO多路复用
I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,比如程序中既要读取鼠标、又要读取键盘,多路读取。
我们可以采用系统调用 select()和 poll()来执行 I/O 多路复用操作。这两个函数基本一样。
13.2.2 select()函数
系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,出现文件描述符成为就绪态,再使用read()/write()去读或写。
#include <sys/select.h>
/* 监控文件描述符是否就绪。-1代表有错误,0代表超时,正数是就绪态的文件个数 */
int select(int nfds, //文件描述符集里,最大文件描述符的值+1,告诉内核忽略所有大于等于该值的文件描述符
fd_set *readfds, //文件描述符集合,要读的
fd_set *writefds, //文件描述符集合,要写的
fd_set *exceptfds, //文件描述符集合,发生异常状况的
struct timeval *timeout);//设置阻塞时间。NULL一直阻塞。timaval结构体秒、纳秒成员
文件描述符集合 fd_set 的所有操作都可以通过四个宏来完成。
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set); //将文件描述符fd从文件描述符集合fd_set移除
void FD_SET(int fd, fd_set *set); //将文件描述符fd添加到文件描述符集合fd_set中
void FD_ZERO(fd_set *set); //将参数set所指向的集合初始化为空
int FD_ISSET(int fd, fd_set *set);//判断文件描述符fd是否在文件描述符集合fd_set中
文件描述符集合有一个最大容量限制,有常量 FD_SETSIZE 来决定,在 Linux 系统下,该常量的值为1024。
在定义一个文件描述符集合之后,必须用 FD_ZERO()宏将其进行初始化操作,然后再添加要监控的文件描述符。
fd_set fset; //定义文件描述符集合
FD_ZERO(&fset); //将集合初始化为空
FD_SET(3, &fset); //向集合中添加文件描述符 3
FD_SET(4, &fset); //向集合中添加文件描述符 4
FD_SET(5, &fset); //向集合中添加文件描述符 5
在调用 select()函数之后,select()函数内部会修改 readfds、writefds、exceptfds 这些集合,当 select()函数返回时,它们包含的就只有已处于就绪态的文件描述符集合了。
如果要在循环中重复调用 select(),我们必须保证每次都要重新初始化并设置 readfds、writefds、exceptfds 这些集合。
13.2.3 poll() 函数
系统调用 poll() 和 select() 很相似,但是参数不同。
select() 的使用需要给出 文件描述符最大值+1、读文件描述符集、写文件描述符集、异常文件描述符集、阻塞时间这些参数。
poll() 则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。
#include <poll.h>
int poll(struct pollfd *fds, //struct pollfd 类型数组
nfds_t nfds, //数组中元素个数
int timeout); //阻塞时间.毫秒
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 用于给出监视的事件 */
short revents; /* 用于获取返回事件 */
};
fd 是一个文件描述符,struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指定文件描述符 fd 需要做检查的事件。
当 poll()函数返回时,revents 变量由 poll()函数内部进行设置,用于存储文件描述符 fd 发生了哪些事件。
将 events 变量设置为 0 代表不询问任何事件;另外,将 fd 变量设置为文件描述符的负值(取文件描述符 fd 的相反数-fd),将导致对应的 events 变量被 poll()忽略,并且 revents变量将总是返回 0,这两种方法都可用来关闭对某个文件描述符的检查。
一般用的最多的还是 POLLIN 和 POLLOUT。
在使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在,下一次调用 select()/poll() 时,文件描述符已经处于就绪态了将直接返回。
13.2.4 epoll
epoll是Linux内核为处理大批量文件描述符而作的改进的poll,是Linux下多路复用IO接口select/poll的增强版本。能显著提高程序在大量并发连接中系统CPU利用率。
epoll所支持的 FD上限是最大可以打开文件的数目,这个数字一般远大于2048。在1GB内存的机器上大约是10万左右,具体数目可以通过cat /proc/sys/fs/file-max查看,这个数目和系统内存关系很大。
epoll不像select或poll一样,每次调用都会线性扫描全部的集合,导致效率现线性下降。
epoll只会对“活跃”的 文件描述符FD 进行操作,因为epoll在内核实现中是根据每个fd上面的callback函数实现的,只有“活跃”的socket才会主动的去调用callback函数。
epoll通过内核与用户空间共享一个事件表来减少系统调用的次数,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。
epoll函数的原型包括四个关键的函数:epoll_create、epoll_ctl、epoll_wait。注意不适用epoll对象时应该使用系统调用close()关闭epoll对象。
/*创建一个epoll实例,即epoll监听集合。创建成功后,返回该epoll对象的描述符*/
int epoll_create(int size);//在Linux内核2.6.8及以后版本中,
//这个参数被忽略,只需传入一个大于0的值即可。
//历史上,这个参数曾用来指定内核需要监控的文件描述符的最大数量,
//但现在内核使用链表来维护文件描述符,因此不再需要这个限制。
int epoll_ctl(int epfd, //eopll对象描述符
int op, //操作
//添加(EPOLL_CTL_ADD)、
//删除(EPOLL_CTL_DEL)、
//修改(EPOLL_CTL_MOD)节点。
int fd, //需要添加、删除、修改的文件描述符
struct epoll_event *event);//指向epoll_event结构体的指针,
//用于指定要监听的事件类型和用户数据。
int epoll_wait(int epfd,
struct epoll_event *events, //用于存放返回的事件集合的数组
int maxevents, //events数组的大小,即最多可以返回的事件数量
int timeout); //等待事件发生的时间,以毫秒为单位。
//如果传入-1,则表示无限期等待
//如果传入0,则表示立即返回,不等待
当用户空间程序调用 epoll_ctl 将文件描述符添加到epoll监听集合时,内核会在事件表中为该文件描述符分配一个事件表。同样地,当文件描述符上的事件发生时,内核会更新事件表中对应条目的状态。
当用户空间程序调用 epoll_wait 时,它会阻塞等待直到有事件发生。此时,内核会检查事件表,将发生事件的文件描述符及其事件类型复制到用户空间提供的epoll_event数组中,并返回发生事件的文件描述符数量。这样,用户空间程序就可以根据返回的信息进行相应的处理。
需要注意的是,虽然我们说“事件表”是内核空间中的一个数据结构,但实际上用户空间程序是通过与内核空间进行交互(通过系统调用)来间接访问这个数据结构的。用户空间程序无法直接访问或修改内核空间中的数据结构。
13.3 异步 IO
在 I/O 多路复用中,进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。
在异步 I/O 中,当文件描述符可以执行 I/O 操作时,内核会给进程发送信号通知。因此异步I/O也被称为信号驱动I/O。
要使用异步 I/O,需要使用 fcntl() 工具函数对文件描述符进行设置:
通过指定 O_NONBLOCK 标志使能非阻塞 I/O。
通过指定 O_ASYNC 标志使能异步 I/O。
F_SETOWN设置异步 I/O 事件的接收进程。
使用signal()/sigaction()为通知信号SIGIO注册一个信号处理函数。
以上步骤完成之后,进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO 信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进行 I/O 操作。以上步骤只能用 fcntl(),不能用open()。
O_ASYNC 标志
O_ASYNC 标志可用于使能文件描述符的异步 I/O 事件,当文件描述符可执行 I/O 操作时,内核会向异步 I/O 事件的接收进程发送 SIGIO 信号(默认情况下)。
可以使用 fcntl()函数 添加 O_ASYNC 标志使能异步 I/O。
int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_ASYNC; //将 O_ASYNC 标志添加到 flag
fcntl(fd, F_SETFL, flag); //重新设置 flag
设置异步 I/O 事件的接收进程
为文件描述符设置异步 I/O 事件的接收进程,也就是设置异步 I/O 的所有者。同样也是通过 fcntl()函数进行设置,操作命令 cmd 设置为 F_SETOWN,第三个参数传入接收进程的进程 ID(PID),通常将调用进程的 PID 传入。
fcntl(fd, F_SETOWN, getpid());
注册 SIGIO 信号的处理函数
通过 signal()或 sigaction()函数为 SIGIO 信号注册一个信号处理函数,当进程接收到内核发送过来的 SIGIO 信号时,会执行该处理函数,所以我们应该在处理函数当中执行相应的 I/O 操作。
signal(SIGIO, sigio_handler);
示例代码演示了以异步 I/O 方式读取鼠标,当进程接收到 SIGIO 信号时,执行信号处理函数 sigio_handler(),在该函数中调用 read()读取鼠标数据。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define MOUSE "/dev/input/event3"
static int fd;
static void sigio_handler(int sig)
{
static int loops = 5;
char buf[100] = {0};
int ret;
if(SIGIO != sig)
return;
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
loops--;
if (0 >= loops) {
close(fd);
exit(0);
}
}
int main(void)
{
int flag;
/* 打开鼠标设备文件<使能非阻塞 I/O> */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 使能异步 I/O */
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 设置异步 I/O 的所有者 */
fcntl(fd, F_SETOWN, getpid());
/* 为 SIGIO 信号注册信号处理函数 */
signal(SIGIO, sigio_handler);
for ( ; ; )
sleep(1);
}
13.4 优化异步 I/O
在一个需要检查大量文件描述符的应用程序中,例如某种类型的网络服务端程序,与 select()和 poll()相比,异步 I/O 能够提供显著的性能优势。
对于 select()或 poll()函数来说,内部实现原理其实是通过轮询的方式来检查多个文件描述符是否可执行 I/O 操作,但由于轮询消耗 CPU 资源,适合需要检查的文件描述符不多时的场景。
当需要检查大量文件描述符时,可以使用 epoll 解决 select()或 poll()性能低的问题。
不管是异步 I/O、还是 epoll,在需要检查大量文件描述符的应用程序当中,在这种情况下,它们的性能相比于 select()或 poll()有着显著的优势。
默认的异步IO存在一些缺陷:
默认的异步 I/O 通知信号 SIGIO 信号是标准信号(非实时信号、不可靠信号),不支持信号排队机制,多次 SIGIO 信号给了进程,这些信号只有当信号处理函数执行完毕之后才会传递给进程,并且只能传递一次,而其它后续的信号都会丢失。
无法得知文件描述符发生了什么事件,也就是只知道有事件发生了,SIGIO信号来了,不知道发生了什么。
13.4.1 使用实时信号替换默认信号 SIGIO
使用 fcntl()函数可以进行设置异步IO不使用标准信号SIGIO,调用函数时将操作命令cmd 参数设置为 F_SETSIG,第三个参数 arg 指定一个实时信号编号即可,表示将该信号作为异步 I/O 通知信号。
fcntl(fd, F_SETSIG, SIGRTMIN);
上述码指定了 SIGRTMIN 实时信号作为文件描述符 fd 的异步 I/O 通知信号,而不再使用默认的 SIGIO信号。
当文件描述符 fd 可执行 I/O 操作时,内核会发送实时信号 SIGRTMIN 给调用进程。
如果第三个参数 arg 设置为 0,则表示指定 SIGIO 信号作为异步 I/O 通知信号,也就是回到了默认状态。 //SIGRTMIN代表信号最小值
13.4.2 使用 sigaction()函数注册信号处理函数
为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指定 SA_SIGINFO,表示指定信号处理函数接收关于信号的附加信息。
因为 sa_sigaction 指向的函数作为信号处理函数提供了更多的参数,可以获取到更多信息。
/* 绑定信号和信号处理函数*/
int sigaction(int signum, //信号
const struct sigaction *act, //不为NULL,表示要为信号设置新的处理方式。
//NULL代表无需改变当前处理方式。
struct sigaction *oldact); //用来获取信号旧的处理方式。可为NULL
struct sigaction {
void (*sa_handler)(int); //信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //替代信号处理函数。
//与信号处理函数互斥,只能设置一个。
sigset_t sa_mask;//信号掩码。被加入信号掩码的信号不能打断当前信号的执行。
int sa_flags; //标志。控制信号的处理过程。
void (*sa_restorer)(void);//弃用
};
传递给信号处理函数的 siginfo_t 结构体中与之相关的字段如下:
si_signo:引发处理函数被调用的信号。这个值与信号处理函数的第一个参数一致。
si_fd:表示发生异步 I/O 事件的文件描述符;
si_code:表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或者是异常事件等。该字段中可能出现的值以及它们对应的描述信息参见表 13.4.1。
si_band:是一个位掩码,返回的事件。其中包含的值与系统调用 poll()中返回的 revents 字段中的值相同。
由此可知,可以在信号处理函数sigaction返回的附加数据中通过对比 siginfo_t 结构体的 si_code 变量来检查文件描述符发生了什么事件,以采取相应的 I/O 操作。
13.5 存储映射 I/O
存储映射 I/O能将一个磁盘物理地址的文件映射到进程内存空间中。
进程直接从内存中读数据时,就相当于 read 文件中的数据;将数据写入这段内存,相当于将数据 write 进文件中。这样就可以在不使用系统调用 read()和 write()的情况下执行 I/O 操作。
memory-mapped I/O,存储映射I/O
mmap,memory map内存映射 munmap,memory unmap 解除内存映射
13.5.1 mmap()和 munmap()函数
系统调用 mmap() 告诉将一个文件映射到进程地址空间中的内存区域中。
系统调用 unmmap() 用来解除映射。
映射和解除映射时指定的地址和解除映射的内存长度,都要遵循映射内存的地址和大小是页大小整数倍的原则。
当调用 munmap()解除映射时并不会将映射区中的内容写到磁盘文件中。
如果 mmap()指定了 MAP_SHARED 标志,映射区内容由内核自动刷新进文件磁盘。
如果 mmap()指定了 MAP_PRIVATE 标志,解除映射后进程对映射区的修改将会丢弃。
#include <sys/mman.h>
/*文件映射*/
void *mmap(void *addr, //内存起始地址
size_t length, //映射长度。文件被映射的部分不能超过文件。
int prot, //映射区权限
int flags, //映射区操作标志
int fd, //文件描述符
off_t offset); //文件映射偏移量
/*
映射区操作标志:
MAP_SHARED:将写入到映射区中的数据更新到文件中,允许其它进程共享。
MAP_PRIVATE:对映射区的任何操作都不会更新到文件中,只对文件副本(copy-on-write)进行读写。
MAP_FIXED:强制使用addr作为映射区起始地址。
MAP_ANONYMOUS:匿名映射.不涉及文件.文件相关参数无效.映射区不和其他进程共享.
MAP_LOCKED:对映射区域上锁。
*/
/*
映射区权限prot
PROT_EXEC :可执行
PROT_READ :可读
PROT_WRITE:可写;
PROT_NONE: 不可访问。
*/
/* 解除映射 */
int munmap(void *addr, size_t length);
对于 mmap()函数,文件起始地址addr 和文件偏移量offset 的值被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,如下所示(以字节为单位):
sysconf(_SC_PAGE_SIZE)
sysconf(_SC_PAGESIZE)
映射长度参数 length 没有这种要求。
即使 mmap 函数给的映射区长度参数 length 不是页大小整数倍,实际分配到的也是整数倍。
假设页大小512字节,文件大小12字节,则系统会提供512字节的映射区。后面的500个字节会被填充成0。可以修改后面的这500个字节,不会影响文件,但读取会发生 SIGBUS异常信号。
SIGBUS,
与映射区相关的两个信号。
SIGSEGV: seg violation。段错误,核心已转储。
SIGBUS:总线错误,内存访问未对齐、内存无效、文件映射异常。会生成核心转储文件。
13.5.2 mprotect()函数
系统调用 mprotect()可以更改映射区的保护权限。
#include <sys/mman.h>
int mprotect(void *addr,//映射区地址
size_t len,//映射区长度
int prot); //映射区权限
/*
映射区权限prot
PROT_EXEC :可执行
PROT_READ :可读
PROT_WRITE:可写;
PROT_NONE: 不可访问。
*/
13.5.3 msync()函数
系统调用 read()和 write() 在操作磁盘文件是在用户空间缓冲区和内核缓冲区之间复制数据,内核在某时刻自动将缓冲区的数据刷新至磁盘,由此可知,write()操作与磁盘操作并不同步。可以使用 库函数 fflush() 或者系统调用 fsync 强制刷新内核缓冲区。
存储 I/O 也是如此,写入文件映射区中的数据不会马上刷新进磁盘。可以调用 msync()函数强制刷新映射区数据到磁盘。
#include <sys/mman.h>
/*系统调用 强制刷新映射区数据到磁盘*/
int msync(void *addr,
size_t length,
int flags); //刷新操作标志
/*
刷新操作标志
MS_ASYNC:以异步方式进行同步操作。调用 msync()函数之后,直接返回,不等待数据全部刷新完。
MS_SYNC: 以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘才返回。
MS_INVALIDATE:使同一文件的其它映射无效(以便可以用刚写入的新值更新它们)。
*/
13.5.4 普通 I/O 与存储映射 I/O 比较
普通 I/O 方式的缺点
普通 I/O 方式一般通过调用 read()和 write()函数来实现对文件的读写,中间经过了很多函数和缓存,效率低。标准 I/O(库函数 fread()、fwrite())涉及用户态和内核态的转换,效率更低。
存储映射 I/O 的优点
存储映射IO的源文件和目标文件都映射到了内存中,直接操作映射区来实现文件复制即可。
存储映射IO实质是建立了用户区和内核区的共享内存。
存储映射 I/O 的不足
所映射的文件只能是固定大小,因为文件所映射的区域已经在调用 mmap()函数时通过 length 参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍,系统自动扩张的区域虽然可以操作,但不能在映射文件中反映出来。
只适合大量数据。少量数据的时候还是普通IO方便。
13.6 文件锁
多个进程对文件进行 I/O 操作时,容易产生竞争状态、导致文件中的内容与预想的不一致。
为了确保多进程环境下的文件安全,出现了文件锁机制。
文件锁的分类
文件锁可以分为建议锁和强制锁两种。
建议锁没拿到锁也能访问。
强制锁必须有锁才能访问。
可以调用建议锁 flock()、强制锁 lockf()、文件描述符管理工具 fcntl()这三个函数对文件上锁。
13.6.1 flock()函数加锁
系统调用 flock() 可以对文件进行建议锁的加锁和解锁。
#include <sys/file.h>
/*文件建议锁*/
int flock(int fd,
int operation);//锁操作
/*
锁操作:
LOCK_SH:上共享锁。
LOCK_EX:上互斥锁。
LOCK_UN:解锁
LOCK_NB:以非阻塞方式上锁。(默认是阻塞上锁)
*/
同一进程使用 flock()对文件多次加锁不会导致死锁,新锁会替换旧锁。
文件关闭的时候,会自动解锁。
一个进程不可以对另一个进程持有的文件锁进行解锁。
由 fork()创建的子进程不会继承父进程持有的锁。
除此之外,当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl() 的 F_DUPFD 操作),这些通过复制得到的文件描述符会和源文件描述符引用同一个锁。使用这些复制的文件描述符的任何一个进行解锁上锁都可以。
flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁
如果不显示的调用一个解锁操作,只有当所有文件描述符都被关闭之后锁才会被释放。
13.6.2 fcntl()函数加锁
fcntl()函数是一个多功能文件描述符管理工具箱,通过配合不同的 cmd 操作命令来实现不同的功能。
flock()仅支持对整个文件进行加锁/解锁。
fcntl()可以对文件的部分区域进行加锁/解锁,甚至精确到某一个字节数据。
flock()仅支持建议锁类型;
fcntl()可支持建议锁和强制锁两种类型。
#include <unistd.h>
#include <fcntl.h>
/*fd 管理工具函数*/
int fcntl(int fd,
int cmd, //指令
... ); // struct flockptr结构体指针
/*
与锁相关的 cmd:
F_GETLK //用来测试,尝试向文件区域加一把flockptr结构体定义的锁,
//失败就代表文件已经有其他进程的锁了,而且现有的锁会改写 flockptr结构体的对象信息。
//成功则会上锁,并将flockptr的locktype改成F_UNLCK
F_SETLK //添加由 flockptr指向的锁。加锁失败则返回,并设置errno。(不阻塞加锁)
F_SETLKW //添加由 flockptr指向的锁。加锁失败则阻塞。(阻塞加锁)
*/
struct flock {
...
short l_type; //锁类型 F_RDLCK,F_WRLCK, F_UNLCK 共享读锁、独占写锁、解锁
short l_whence; //偏移量的基准未知 SEEK_SET, SEEK_CUR, SEEK_END 起始、当前、结尾
off_t l_start; //解锁的起始地址偏移量
off_t l_len; //上锁的长度
pid_t l_pid; //该锁能阻塞的进程,cmd=F_GETLK时有效
...
};
文件关闭的时候,自动解锁。
一个进程不可以对另一个进程持有的文件锁进行解锁。
由 fork()创建的子进程不会继承父进程的锁。
使用 dup()、dup2()或 fcntl()的 F_DUPFD 操作,复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个都可以进行解锁,这点与 flock()是一样的。
建议性锁和强制性锁
一般不建议使用强制性锁,所以大部分情况下使用的都是建议性锁。
开启强制性锁机制主要跟文件的权限位有关。
如果要开启强制性锁机制,需要将文件的设置组ID位(S_ISGID)位置 1,并且禁止文件的组用户执行权限(S_IXGRP),也就是将其设置为 0。
但是,有些 Linux/Unix 发行版系统并不支持强制性锁机制。
/* 开启强制性锁机制 */
if (0 > fstat(fd, &sbuf)) {//获取文件属性
perror("fstat error");
exit(-1);
}
if (0 > fchmod(fd,
(sbuf.st_mode & ~S_IXGRP) | S_ISGID)) {
perror("fchmod error");
exit(-1);
}
如果 A进程已经对文件设置了写锁,B进程试图对文件设置读锁时,将会失败;
B进程在没有获取到读锁的情况下,调用 read()读取文件将会出现两种情况:
如果系统支持强制性锁机制,那么 read()将会失败;
如果系统不支持强制性锁机制,read()将会成功。