13. 高级IO
- 1. 非阻塞 IO
- 1.1 阻塞 IO 与非阻塞 IO 读文件
- 2. IO 多路复用
- 2.1 何为 IO 多路复用
- 2.2 select()
- 2.3 poll()
- 2.3.1 struct pollfd
- 2.3.2 poll() 返回值
- 2.3.3 示例
- 3. 异步 IO
- 3.1 O_ASYNC
- 3.2 设置异步 IO 事件的接收进程
- 3.3 示例
- 4. 优化异步 IO
- 4.1 使用实时信号替换默认信号 SIGIO
- 4.2 使用 sigaction() 注册信号处理函数
- 4.3 示例
- 5. 存储映射 IO
- 5.1 mmap() 和 munmap()
- 5.1.1 mmap()
- 5.1.2 munmap() 解除映射
- 5.2 mprotect()
- 5.3 msync()
- 5.4 普通 IO 与存储映射 IO 比较
- 6. 文件锁
- 6.1 flock() 函数加锁
- 6.2 fcntl() 函数加锁
- 6.2.1 struct flock
- 6.2.2 两种类型的锁:F_RDLCK 和 F_WRLCK
- 6.2.3 F_SETLK、 F_SETLKW 和 F_GETLK
1. 非阻塞 IO
非阻塞 IO 就是对文件的 IO 操作都是非阻塞的。譬如对文件的读操作时,如果数据未准备好,那么读操作就可能被阻塞,如果是非阻塞 IO 就不会阻塞,而是立马返回错误。
普通文件的读写操作是不会阻塞的,但对于管道文件、设备文件等,可以是阻塞式也可以是非阻塞式。
1.1 阻塞 IO 与非阻塞 IO 读文件
尝试两种方式打开鼠标文件。鼠标文件在 /dev/input
目录下,有些设备是 mouse,有些是 event,可以自己使用sudo od -x /dev/input/event3
命令实验,然后按下鼠标,如果有数据打印,就表明是鼠标文件。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
char buf[100];
int fd, ret;
fd = open("/dev/input/event3", O_RDONLY);
// 非阻塞方式
// fd = open("/dev/input/event3", O_RDONLY|O_NONBLOCK);
if (-1 == fd)
{
perror("open:");
exit(-1);
}
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (0 > ret)
{
perror("read:");
close(fd);
exit(-1);
}
printf("成功读取<%d>个字节数据\n", ret);
close(fd);
exit(0);
}
运行后,发现没有任何信息打印,那么就尝试移动鼠标,阻塞就会结束,并打印信息。如果是非阻塞方式,建议使用轮询检查的方式。
int main()
{
char buf[100];
int fd,ret;
fd = open("/dev/input/event3",O_RDONLY|O_NONBLOCK);
memset(buf,0,sizeof(buf));
ret=read(fd,buf,sizeof(buf));
while(ret<0)
{
perror("read");
sleep(1);
ret=read(fd,buf,sizeof(buf));
}
cout << "读取到 " << ret<<" 字节的数据"<<endl;
close(fd);
return 0;
}
阻塞式 IO 不能够进行并发读取文件,需要使用非阻塞 IO,但是仍然不够好。
2. IO 多路复用
2.1 何为 IO 多路复用
IO 多路复用是通过一种机制,可以监视多个文件描述符。一般用于并发式的非阻塞 IO。IO 多路复用是外部阻塞式,内部监视多路 IO。 通过以下两个系统调用来执行复用操作。
2.2 select()
调用该函数会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。
#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
/*
* readfds:检测是否可读的文件描述符集合
* writefds:检测是否可写的文件描述符集合
* execptfds:检测影厂情况是否发生的文件描述符集合
* nfds:通常表示最大文件描述符编号值加1,也就是在上面三个文件描述符集合中找出最大的再加1
* timeout:设定select() 阻塞的时间上限,可以设置为NULL表示一直会阻塞,直到有文件描述符就绪
* /
fd_set 类型是一个文件描述符的集合体,是以位掩码的形式来实现的,但是我们是使用宏来完成操作的。如果不关心,可以设置为 NULL,那么该函数就类似于一个休眠函数。
如果timeout 指向的结构体中两个成员变量都是 0,那么函数就不会被阻塞,只是轮询指定的文件描述符集合,看其中是否有就绪的。
函数将被阻塞,直到有以下事件发生:
- 指定的文件描述符中至少有一个是就绪态
- 函数被信号处理函数中断
- timeout 指定的时间上限已经超时
宏函数:
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set); // 将fd从set指向的集合中移除
int FD_ISSET(int fd, fd_set *set); // 如果fd是set中的成员,就返回true,否则返回false
void FD_SET(int fd, fd_set *set); // 将fd添加到set中
void FD_ZERO(fd_set *set); // 将set指向的集合初始化为空
文件描述符集合有一个最大容量,用常量 FD_SETSIZE 来决定。
返回值:
- -1 表示有错误发生,详细的错误可以使用 man 手册查看
- 0 表示在任何文件描述符就绪前函数已经超时,这时三个文件描述符集合都会清空
- 返回一个正数表示有文件描述符就绪,返回值表示就绪的文件描述符个数。在这种情况下,每个返回的文件描述符集合都需要检查,通过 FD_ISSET()宏进行检查,以此找出发生的 I/O 事件是什么。如果同一个文件描述符在 readfds, writefds 以及 exceptfds 中同时被指定,且它多于多个 I/O 事件都处于就绪态的话,那么就会被统计多次,换句话说, select()返回三个集合中被标记为就绪态的文件描述符的总数。
2.3 poll()
#include <poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
fds: 指向一个数组,数组中每一个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件
nfds: 指定了 fds 中元素个数,实际为无符号整型
timeout: 为 -1,会一直阻塞,直到有文件描述符达到就绪或者捕捉到一个信号时返回;如果为 0,不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态;大于 0,表示设置 poll() 函数阻塞时间的上限值,意味着 poll() 函数最多阻塞 timeout 毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
2.3.1 struct pollfd
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fd 是一个文件描述符,events 和 revents 都是位掩码,调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件。当 poll() 返回时,revents 由 poll() 内部进行设置,用于说明文件描述符 fd 发生了哪些事件。
应将每个数组元素的 events 成员设置为表中所示的一个或几个标志,多个标志通过位或运算符组合起来,通过这些值告诉内核我们关心的是该文件描述符的哪些事件。同样,返回时,revents 变量由内核设置为表中所示的一个或几个标志。
第一组标志与数据可读相关;第二组标志与可写数据相关;而第三组标志是设定在 revents 变量中用来返回有关文件描述符的附加信息,如果在 events 变量中指定了这三个标志,则会被忽略。
如果对某个文件描述符上的事件不感兴趣,可将 events 变量设置为 0;另外,将 fd 变量设置为文件描述符的相反数,会导致对应的 events 被忽略,并且 revents 将总是返回 0. 这两种方式都可以用来关闭对某个文件描述符的检查。
2.3.2 poll() 返回值
- -1 表示有错误发生,并且设置 errno
- 0 表示该调用在任意一个文件描述符成为就绪态之前就已经超时了
- 返回一个正整数表示有一个或多个文件描述符处于就绪态,返回值表示数组 fds 中返回的 revents 变量不为 0 的 struct pollfd 对象的数量
2.3.3 示例
实现 IO 复用,同时读取键盘和鼠标。
#define MOUSE "/dev/input/event3"
int main()
{
char buf[100];
int fd,ret=0,flag;
int loops=5;
struct pollfd fds[2];
fd=open(MOUSE,O_RDONLY|O_NONBLOCK);
if(fd==-1)
{
perror("open");
return -1;
}
// 将键盘设置为非阻塞方式
flag=fcntl(0,F_GETFL); // 获取原来的flag
flag|=O_NONBLOCK;
fcntl(0,F_SETFL,flag); // 重新设置flag
// 同时读取键盘和鼠标
fds[0].fd=0;
fds[0].events=POLLIN;
fds[0].revents=0;
fds[1].fd=fd;
fds[1].events=POLLIN;
fds[1].revents=0;
while(loops--)
{
ret=poll(fds,2,-1);
if(ret<0)
{
perror("poll");
close(fd);
return -1;
}
else if(ret==0)
{
cout << "poll timeout"<<endl;
continue;
}
// 检查键盘是否是就绪态
if(fds[0].revents&POLLIN)
{
ret=read(0,buf,sizeof(buf));
if(ret>0)
{
cout << "键盘读取 " << ret<<" 个字节数据"<<endl;
}
}
if(fds[1].revents&POLLIN)
{
ret=read(fd,buf,sizeof(buf));
if(ret>0)
{
cout << "鼠标读取 " << ret<<" 个字节数据"<<endl;
}
}
}
}
3. 异步 IO
在异步 IO 当中,当文件描述符可以执行 IO 操作时,进程可以请求内核为自己发送一个信号,之后进程就可以执行其它的任务直到文件描述符可以执行 IO 操作为止,此时内核会发送信号给进程。
要使用异步 IO,程序要按照以下步骤来执行:
- 通过指定 O_NONBLOCK 标志使能非阻塞 IO
- 通过指定 O_ASYNC 标志使能异步 IO
- 设置异步 IO 事件的接收进程,也就是当文件描述符可执行 IO 操作时会发送信号通知该进程,通常将调用进程设置为异步 IO 事件的接收进程
- 为内核发送的通知信号注册一个信号处理函数。默认情况下,异步 IO 的通知信号是 SIGIO
- 以上步骤完成后,进程就可以执行其他任务了,当接收到信号时,会执行预先注册好的信号处理函数,在信号处理函数中进行 IO 操作。
3.1 O_ASYNC
可用于使能文件描述符的异步 IO 事件,当文件描述符可执行 IO 操作时,内核会向异步 IO 事件的接收进程发送 SIGIO 信号。在调用 open() 时无法通过指定 O_ASYNC 标志来使能异步 IO,但可以使用 fcntl() 添加 O_ASYNC 来使能异步 IO。
3.2 设置异步 IO 事件的接收进程
为文件描述符设置异步 IO 事件的接收进程,也就是设置异步 IO 的所有者。
fcntl(fd,F_SETOWN,getpid());
3.3 示例
#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(ret>0)
{
printf("鼠标成功读取到 %d 个字节数据\n",ret);
}
loops--;
if(loops<=0)
{
close(fd);
exit(0);
}
}
int main()
{
int flag;
fd=open(MOUSE,O_RDONLY|O_NONBLOCK);
if(fd==-1)
{
perror("open");
return -1;
}
flag=fcntl(fd,F_GETFL);
flag|=O_ASYNC;
fcntl(fd,F_SETFL,flag);
// 设置异步 IO 所有者
fcntl(fd,F_SETOWN,getpid());
signal(SIGIO,sigio_handler);
for(;;)
{
cout << "------"<<endl;
sleep(1);
}
return 0;
}
4. 优化异步 IO
默认的异步 IO 通知信号 SIGIO 是非排队信号,如果当前正在执行 SIGIO 信号的处理函数,此时内核又发送多次 SIGIO 信号给进程,这些信号将会被阻塞,只有当信号处理函数执行完毕后才会传递给进程,并且只能传递一次,而其它后续的信号都会丢失。而且无法得知文件描述符发生了什么事件。
4.1 使用实时信号替换默认信号 SIGIO
使用 fcntl() 将 cmd 设置为 F_SETSIG,将最后一个参数指定为一个实时信号即可,表示将该信号作为异步 IO 通知信号。如fcntl(fd,F_SETSIG,SIGRTMIN);
,如果最后一个参数设置为 0,表示回到默认状态。
4.2 使用 sigaction() 注册信号处理函数
在应用程序当中需要为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指定 SA_SIGINFO, 表示使用 sa_sigaction 指向的函数作为信号处理函数,而不使用 sa_handler 指向的函数。因为 sa_sigaction 指向的函数作为信号处理函数提供了更多的参数,可以获取到更多信息。
函数参数包括一个 siginfo_t 指针,指向 siginfo_t 类型对象,当触发信号时该对象由内核构建。siginfo_t 结构体中提供了很多信息,我们可以在信号处理函数中使用这些信息。对于异步 IO 事件而言,传递给信号处理函数的 siginfo_t 结构体中与之相关的字段如下:
- si_signo: 引发处理函数被调用的信号
- si_fd: 表示发生异步 IO 事件的文件描述符
- si_code: 表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或异常事件等。
- si_band: 是一个位掩码,其中包含的值与系统调用 poll() 中返回的 revents 字段中的值相同。
4.3 示例
#include <iostream>
using namespace std;
#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 io_handler(int sig,siginfo_t *info,void *context)
{
static int loops=5;
char buf[100]={0};
int ret;
if(SIGRTMIN != sig)
{
return;
}
// 判断鼠标是否可读
if(POLL_IN==info->si_code)
{
ret=read(fd,buf,sizeof buf);
if(ret>0)
{
printf("鼠标读取到 %d 个字节数据\n",ret);
}
loops--;
if(loops<=0)
{
close(fd);
exit(0);
}
}
}
int main()
{
struct sigaction act;
int flag;
fd=open(MOUSE,O_RDONLY|O_NONBLOCK);
if(fd==-1)
{
perror("open");
return -1;
}
// 使能异步 IO
flag=fcntl(fd,F_GETFL);
flag|=O_ASYNC;
fcntl(fd,F_SETFL,flag);
// 设置异步IO 的所有者
fcntl(fd,F_SETOWN,getpid());
// 指定实时信号SIGTMIN为异步IO通知信号
fcntl(fd,F_SETSIG,SIGRTMIN);
// 注册信号处理函数
act.sa_sigaction=io_handler;
act.sa_flags=SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGRTMIN,&act,NULL);
while(1)
{
cout <<"--------"<<endl;
sleep(1);
}
return 0;
}
5. 存储映射 IO
存储映射 IO(memory-mapped IO) 是一种基于内存区域的高级 IO 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于对文件进行读操作;将数据写入到这段内存就相当于对文件进行写操作。这样就可以在不使用基本 IO 操作函数时执行 IO 操作。
5.1 mmap() 和 munmap()
5.1.1 mmap()
为了实现存储映射 IO 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中。
#include <sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
参数:
- addr: 指定映射到内存区域的起始地址,通常设置为 NULL,表示由系统选择
- length: 指定映射长度,以字节为单位
- offset: 文件映射的偏移量,通常设置为 0,表示从文件头部开始映射
- fd: 指定要映射到内存的文件
- prot: 指定映射区的保护要求,保护要求不能超多文件 open() 时的访问权限,可取值:
- PROT_EXEC: 映射区可执行
- PROT_READ: 映射区可读
- PROT_WRITE: 映射区可写
- PROT_NONE: 映射区不可访问
- flags: 可影响映射区的多种属性,必须指定以下两种标志之一:
- MAP_SHARED: 写入数据时,会写入到文件中,并且允许其它进程共享
- MAP_PRIVATE: 写入数据时,会写入到文件的一个副本中,不会更新到文件。
除此之外,还可将一些进行或运算,可使用 man 手册查看。
返回值:
成功时,返回值就是映射区的起始地址;发生错误时,返回 (void*)-1,通常使用 MAP_FAILED 来表示。
参数 addr 和 offset 不为 NULL 和 0 的情况下,addr 和 offset 通常被要求为系统页大小的整数倍,可通过 sysconf() 函数获取页大小。sysconf(_SC_PAGESIZE)
。虽然对 addr 和 offset 有这种限制,但对于参数 length 长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?对于这个问题的答案,我们首先需要了解到,对于 mmap()函数来说,当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数 length并不是页大小的整数倍。如果文件大小为 96 个字节,我们调用 mmap() 时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为0,可以修改后面的这 4000 个字节,但是并不会影响到文件。但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。而且 length 不能大于文件大小。
与映射区相关的两个信号
- SIGSEGV: 如果映射区被 mmap() 指定成了只读的,那么进程试图将数据写入到该映射区时,将会产生 SIGSEGV 信号,此信号由内核发送给进程。该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
- SIGBUS: 如果映射区的某个部分在访问时已不存在,则会产生 SIGBUS 信号。 例如,调用 mmap() 进行映射时,将参数 length 设置为文件长度,但在访问映射区之前,另一个进程已将该文件截断(譬如调用 ftruncate()函数进行截断),此时如果进程试图访问对应于该文件已截去部分的映射区,进程将会受到内核发送过来的 SIGBUS 信号,同样,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
5.1.2 munmap() 解除映射
当不再需要时,必须解除映射。
#include <sys/mman.h>
int munmap(void *addr,size_t length);
addr 指定待解除映射地址范围的起始地址,length 指定解除映射区域的大小,也必须是系统页大小的整数倍。当进程终止时,也会自动解除映射,但调用 close() 关闭文件时并不会解除映射。
通常将 addr 设置为 mmap() 的返回值,length 设置为 mmap() 的 length,表示解除整个由 mmap() 创建的映射。
5.2 mprotect()
可以更改一个现有映射区的保护要求
#include <sys/mman.h>
int mprotect(void *addr,size_t len,int prot);
将指定地址范围的保护要求更改为 prot 指定的类型,和 mmap() 的prot 一样。
5.3 msync()
将数据刷新到硬盘文件中
#include <sys/mman.h>
int msync(void *addr,size_t length,int flags);
参数 flags 应指定为 MS_ASYNC 和 MS_SYNC 两个标志之一, 除此之外,还可以根据需求选择是否指定 MS_INVALIDATE 标志,作为一个可选标志。
- MS_ASYNC: 以异步方式进行同步操作。调用 msync()函数之后,并不会等待数据完全写入磁盘之后才返回。
- MS_SYNC: 以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘之后才返回。
- MS_INVALIDATE: 是一个可选标志, 请求使同一文件的其它映射无效(以便可以用刚写入的新值更新它们)
函数调用成功返回 0,失败返回 -1. munmap() 并不会将映射区中的内容写到磁盘文件中,如果 mmap() 指定了 MAP_SHARED 标志,对于文件的更新,会在我们将数据写入到映射区之后的某个时刻将映射区中的数据更新到磁盘文件中。如果 mmap() 指定了 MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃。
5.4 普通 IO 与存储映射 IO 比较
普通 IO 的缺点:
普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写, 使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。 同样使用标准 I/O(库函数 fread()、 fwrite())也是如此,本身标准 I/O 就是对普通 I/O 的一种封装。
那既然效率较低,为啥还要使用这种方式呢?原因在于,只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的 I/O 方式还是非常方便的
存储映射 I/O 的优点:
存储映射 I/O 的实质其实是共享, 与 IPC 之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O 方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中,而对于存储映射 I/O 来说,由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映射区来实现文件复制。
存储映射 I/O 的实质其实是共享,如何理解共享呢?其实非常简单, 我们知道,应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操作硬件。通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()、 write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。
存储映射 I/O 的不足:
存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的区域已经在调用 mmap() 函数时通过 length 参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍,譬如映射文件的大小为 96 字节,假定系统页大小为 4096 字节,那么剩余的 4000 字节全部填充为 0,虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来,由此可知,使用存储映射 I/O 在进行大数据量操作时比较有效;对于少量数据,使用普通 I/O 方式更加方便!
存储映射 I/O 的应用场景:
由上面介绍可知,存储映射 I/O 在处理大量数据时效率高,对于少量数据处理不是很划算,所以通常来说,存储映射 I/O 会在视频图像处理方面用的比较多
6. 文件锁
文件锁和互斥锁都是内核提供的,用于对共享资源的访问进行保护。不过,互斥锁、自旋锁和读写锁主要用于多线程环境下,做到线程同步。而文件锁是避免多个进程同时操作同一文件时发生错误。
文件锁分为建议性锁和强制性锁,建议性锁就是一种协议,有些进程可以不遵循这个协议;而强制性锁就是每个进程都应该遵循的,在没有获取到文件锁的时候无法对文件进行访问。
6.1 flock() 函数加锁
#include <sys/file.h>
int flock(int fd,int operation);
/* operation:
* LOCK_SH: 放置一把共享锁,可以被多个进程同时拥有
* LOCK_EX: 放置一把互斥锁,只能被一个进程拥有
* LOCK_UN: 解锁
* LOCK_NB: 以非阻塞方式获取锁。默认时,flock() 无法获取到文件锁会阻塞,如果设置了该选项,无法获取锁时立即返回错误,errno 设置为 EWOULDBLOCK
* /
可以对文件进行加锁或解锁,但是只能产生建议性锁。函数成功返回 0,失败返回 -1。对于 flock(), 同一个文件不会同时具有共享锁和互斥锁。
对同一文件多次加锁不会导致死锁,新的锁会替换旧的锁;文件关闭的时候,会自动解锁;一个进程不可以对另一个进程持有的文件锁进行解锁;由 fork() 创建的子进程不会继承父进程所创建的锁;当一个文件描述符被复制时,通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,都可以进行解锁。
6.2 fcntl() 函数加锁
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
与锁相关的 cmd 为 F_SETLK、 F_SETLKW、 F_GETLK, 第三个参数 flockptr 是一个 struct flock 结构体指针。 使用 fcntl()实现文件锁功能与 flock()有两个比较大的区别:
- flock() 仅支持对整个文件进行加锁或解锁,而 fcntl() 可以对文件的某个区域,可以精确的某一个字节数据。
- fcntl() 可以支持建议性锁和强制性锁。
6.2.1 struct flock
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock(set by F_GETLK and F_OFD_GETLK) */
...
};
- l_type:所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一, F_RDLCK 表示共享性质的读锁,F_WRLCK 表示独占性质的写锁,F_UNLCK 表示解锁一个区域。
- l_whence 和 l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量
- l_len: 需要加锁或解锁区域的字节长度。
- l_pid: 一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。
以上便是对 struct flock 结构体各成员变量的简单介绍,对于加锁和解锁区域的说明,还需要注意以下几项规则: - 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
- 若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始, 到文件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的, 这意味着不管向该文件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
- 如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置, 并且指定参数 l_len 等于 0。
6.2.2 两种类型的锁:F_RDLCK 和 F_WRLCK
任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定的字节上只能有一个进程有一把独占写锁,进一步而言,如果在一个给定的字节上已经有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何锁(包括读锁和写锁)。
如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新加的锁将替换旧的锁。譬如,若某一进程在文件的 100 ~ 200 字节区间有一把写锁,然后又试图在 100 ~ 200 字节区间再加一把读锁,那么该请求将会成功执行,原来的写锁会替换为读锁。还需要注意另外一个问题, 当对文件的某一区域加读锁时,调用进程必须对该文件有读权限,譬如 open() 时 flags 参数指定了 O_RDONLY 或 O_RDWR;当对文件的某一区域加写锁时,调用进程必须对该文件有写权限,譬如 open()时 flags 参数指定了O_WRONLY 或 O_RDWR。
6.2.3 F_SETLK、 F_SETLKW 和 F_GETLK
- F_GETLK: 这种用法一般用于测试,测试调用进程对文件加一把由参数 flockptr 指向的 struct flock 对象所描述的锁是否会加锁成功。如果加锁不成功,意味着该文件的这部分区域已经存在一把锁,并且由另一进程所持有,并且调用进程加的锁与现有锁之间存在排斥关系, 现有锁会阻止调用进程想要加的锁, 并且现有锁的信息将会重写参数 flockptr 指向的对象信息。 如果不存在这种情况,也就是说 flockptr 指向的 struct flock 对象所描述的锁会加锁成功,则除了将 struct flock 对象的 l_type修改为 F_UNLCK 之外,结构体中的其它信息保持不变。
- F_SETLK: 对文件添加由 flockptr 指向的 struct flock 对象所描述的锁。譬如试图对文件的某一区域加读锁(l_type 等于 F_RDLCK)或写锁(l_type 等于 F_WRLCK),如果加锁失败,那么 fcntl() 将立即出错返回,此时将 errno 设置为 EACCES 或 EAGAIN。也可用于清除由 flockptr 指向的 struct flock 对象所描述的锁(l_type 等于 F_UNLCK)
- F_SETLKW: 此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进程将会进入阻塞状态。 只有当请求的锁可用时,进程才会被唤醒。
F_GETLK 命令一般很少用, 事先用 F_GETLK 命令测试是否能够对文件加锁,然后再用 F_SETLK 或 F_SETLKW 命令对文件加锁,但这两者并不是原子操作, 所以即使测试结果表明可以加锁成功,但是在使用 F_SETLK 或 F_SETLKW 命令对文件加锁之前也有可能被其它进程锁住。
使用 fcntl() 创建锁的规则与 flock() 一样,文件关闭的时候自动解锁;一个进程不能对另一个进程进行解锁;不能够被继承;同样,也可以进行复制文件描述符。
fcntl() 支持强制锁,但是一般不建议使用。