参考引用
$ cd /dev/input
$ ls -lh
总用量 0
drwxr-xr-x 2 root root 80 1月 8 20:18 by-id
drwxr-xr-x 2 root root 180 1月 8 20:18 by-path
crw-rw---- 1 root input 13, 64 1月 8 20:18 event0
crw-rw---- 1 root input 13, 65 1月 8 20:18 event1
crw-rw---- 1 root input 13, 66 1月 8 20:18 event2
crw-rw---- 1 root input 13, 67 1月 8 20:18 event3
crw-rw---- 1 root input 13, 68 1月 8 20:18 event4
crw-rw---- 1 root input 13, 69 1月 8 20:18 event5
crw-rw---- 1 root input 13, 70 1月 8 20:18 event6
crw-rw-r-- 1 root input 13, 0 1月 8 20:18 js0
crw-rw-r-- 1 root input 13, 1 1月 8 20:18 js1
crw-rw---- 1 root input 13, 63 1月 8 20:18 mice
crw-rw---- 1 root input 13, 32 1月 8 20:18 mouse0
crw-rw---- 1 root input 13, 33 1月 8 20:18 mouse1
crw-rw---- 1 root input 13, 34 1月 8 20:18 mouse2
# Ubuntu 中,普通用户无法对设备文件进行读取或写入操作,因此需要加 sudo 执行
# 当执行命令之后,移动鼠标或按下鼠标、松开鼠标都会在终端打印出相应的数据,证明 mouse2 是鼠标对应的设备文件
$ sudo od -x /dev/input/mouse2
0000000 c138 2881 8100 0028 2881 fd00 ff18 1800
0000020 7ffd 0008 087f 3900 fe38 38fd fefe fd38
0000040 38fd f7f7 f238 38f2 f2f3 ef38 38f0 f0f0
0000060 ec38 38ef c7ae d238 38f3 ffe9 ed18 1800
0000100 08eb eb18 180b 09f5 f618 180a 06fd fe18
0000120 0808 2a00 0408 0813 1807 0d08 0823 3e1c
0000140 0c08 0814 271d 1008 080f 090f 0608 0802
0000160 0105 0408 0800 0003 0128 28fd fd02 0028
0000200 28ff f703 0128 28fe f803 0128 28fd f802
0000220 0128 28fa fe01 0028 28fe fd03 0128 28fe
0000240 ff01 0128 28ff fe03 0108 0800 0001 0108
0000260 0800 0001 0108 0800 0001 0108 0800 0003
0000300 1208 0801 0001 0208 0800 0001 0408 0800
0000320 0001 0208 0800 0005 0728 28ff fe0c 0528
0000340 28fd fb0d 0928 28fa fd05 0328 28fc f806
0000360 0828 28e5 f600 0128 28fa ed00 0028 38f3
0000400 d5f6 fc38 38f3 f8fd fe38 38fa f6fb fb38
0000420 38f1 fbfd ff38 38fc fcfe 0028 28ff ff00
0000440 ff18 2800 ff00 0028 28ff ff00 ff38 28ff
#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(void) {
char buf[100];
int fd, ret;
// 打开文件
fd = open("/dev/input/mouse2", O_RDONLY);
if (fd == -1) {
perror("open error");
exit(-1);
}
// 读文件
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (ret < 0) {
perror("read error");
close(fd);
exit(-1);
}
printf("成功读取<%d>个字节数据\n", ret);
/* 关闭文件 */
close(fd);
exit(0);
}
$ gcc block.c -o block
# 执行程序没有立即结束,而是一直占用了终端没有输出信息,原因在于调用 read() 之后进入了阻塞状态,因为当前鼠标没有数据可读
# 如果此时移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read() 会成功读取到数据并返回
$ sudo ./block
成功读取<3>个字节数据
$
...
fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
...
$ gcc block2.c -o block2
$ sudo ./block2
# 执行程序后立马就结束了,并且调用 read() 返回错误
# 意思就是说资源暂时不可用;原因在于调用 read() 时,如果鼠标并没有移动或者被按下(没有发生输入事件),是没有数据可读,故而导致失败返回,这就是非阻塞 I/O
read error: Resource temporarily unavailable
#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(void) {
char buf[100];
int fd, ret;
/* 打开文件 */
fd = open("/dev/input/mouse2", O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 轮训读文件 */
memset(buf, 0, sizeof(buf));
for (;;) {
ret = read(fd, buf, sizeof(buf));
if (0 < ret) {
printf("成功读取<%d>个字节数据\n", ret);
close(fd);
exit(0);
}
}
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MOUSE "/dev/input/mouse2"
int main(void) {
char buf[100];
int fd, ret;
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY);
if (fd == -1) {
perror("open error");
exit(-1);
}
/* 读鼠标 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
/* 读键盘 */
memset(buf, 0, sizeof(buf));
ret = read(0, buf, sizeof(buf));
printf("键盘: 成功读取<%d>个字节数据\n", ret);
/* 关闭文件 */
close(fd);
exit(0);
}
$ gcc test.c -o test
# 需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘
$ sudo ./test
鼠标: 成功读取<3>个字节数据
hunan
键盘: 成功读取<6>个字节数据
因为 read() 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行,这就是阻塞式 I/O 的一个困境:无法实现并发读取(同时读取),主要原因在于阻塞
- 方法一:使用多线程,一个线程读取鼠标、另一个线程读取键盘
- 方法二:者创建一个子进程,父进程读取鼠标、子进程读取键盘
- 方法三:使用非阻塞式 I/O
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MOUSE "/dev/input/mouse2"
int main(void) {
char buf[100];
int fd, ret, flag;
/* 非阻塞方式打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (fd == -1) {
perror("open error");
exit(-1);
}
// 标准输入文件描述符(键盘)是从其父进程进程而来,并不是在程序中调用 open() 打开得到
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); // 先获取原来的 flag
flag |= O_NONBLOCK; // 将 O_NONBLOCK 标准添加到 flag
fcntl(0, F_SETFL, flag); // 重新设置 flag
for (;;) {
/* 读鼠标 */
ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
/* 读键盘 */
ret = read(0, buf, sizeof(buf));
if (ret > 0) {
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
}
/* 关闭文件 */
close(fd);
exit(0);
}
$ gcc test2.c -o test2
# 不管是先动鼠标还是先按键盘都可以成功读取到相应数据
$ sudo ./test2
hunan # 键盘输入
键盘: 成功读取<6>个字节数据
鼠标: 成功读取<3>个字节数据
鼠标: 成功读取<3>个字节数据
鼠标: 成功读取<3>个字节数据
changsha # 键盘输入
键盘: 成功读取<9>个字节数据
鼠标: 成功读取<3>个字节数据
鼠标: 成功读取<3>个字节数据
鼠标: 成功读取<3>个字节数据
虽然使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题,但使得程序的 CPU 占用率特别高,为了解决这个问题,就要用到 I/O 多路复用方法
调用 select() 函数将阻塞直到有以下事情发生
#include <sys/select.h>
// nfds:在 readfds、writefds 和 exceptfds 3 个描述符集中找出最大描述符编号值,然后加 1
// timeout:可用于设定 select() 阻塞的时间上限,控制 select 的阻塞行为
// 1、可将 timeout 设置为 NULL,表示 select() 将会一直阻塞,直到某一个或多个文件描述符成为就绪态
// 2、也可将其指向一个 struct timeval 结构体对象,如果结构体对象中的两个成员变量都为 0
// 那么此时 select() 函数不会阻塞,只是简单地轮训指定的文件描述符集合,看其中是否有就绪的文件描述符并立刻返回
// 否则,参数 timeout 将为 select() 指定一个等待(阻塞)时间的上限值
// 如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回
// 如果超过了阻塞时间的上限值,select() 函数将会返回
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
fd_set 数据类型(以位掩码的形式来实现)是一个文件描述符的集合体
位掩码
所有关于文件描述符集合的操作都是通过四个宏完成:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set); // 将文件描述符 fd 从参数 set 所指向的集合中移除
int FD_ISSET(int fd, fd_set *set); // 如果 fd 是参数 set 所指向集合中的成员,则 FD_ISSET() 返回 true,否则返回 false
void FD_SET(int fd, fd_set *set); // 将 fd 添加到参数 set 所指向的集合中
void FD_ZERO(fd_set *set); // 将参数 set 所指向的集合初始化为空
// 文件描述符集合有一个最大容量限制,由常量 FD_SETSIZE 决定,Linux 系统该常量的值为 1024
// 如果要在循环中重复调用 select(),必须保证每次都要重新初始化并设置 readfds、writefds、exceptfds 这些集合
fd_set fset; // 定义文件描述符集合
FD_ZERO(&fset); // 将集合初始化为空
FD_SET(3, &fset); // 向集合中添加文件描述符 3
FD_SET(4, &fset); // 向集合中添加文件描述符 4
FD_SET(5, &fset); // 向集合中添加文件描述符 5
返回值
示例:使用 select 实现同时读取键盘和鼠标(非阻塞 I/O 方式)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#define MOUSE "/dev/input/mouse2"
int main(void) {
char buf[100];
int fd, ret = 0, flag;
fd_set rdfds;
int loops = 5;
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); // 先获取原来的 flag
flag |= O_NONBLOCK; // 将 O_NONBLOCK 标准添加到 flag
fcntl(0, F_SETFL, flag); // 重新设置 flag
/* 同时读取键盘和鼠标 */
while (loops--) {
FD_ZERO(&rdfds);
FD_SET(0, &rdfds); // 添加键盘
FD_SET(fd, &rdfds); // 添加鼠标
ret = select(fd + 1, &rdfds, NULL, NULL, NULL);
if (ret < 0) {
perror("select error");
goto out;
} else if (ret == 0) {
fprintf(stderr, "select timeout.\n");
continue;
}
/* 检查键盘是否为就绪态 */
if (FD_ISSET(0, &rdfds)) {
ret = read(0, buf, sizeof(buf));
if (ret > 0) {
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
}
/* 检查鼠标是否为就绪态 */
if (FD_ISSET(fd, &rdfds)) {
ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
}
}
out:
/* 关闭文件 */
close(fd);
exit(ret);
}
在 poll() 函数中,需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及对该文件描述符所关心的条件(数据可读、可写或异常情况)
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// 调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件
// 当 poll() 函数返回时,revents 变量由 poll() 函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 位掩码:requested events */
short revents; /* 位掩码:returned events */
};
poll 的 events 和 revents 标志
返回值
示例:使用 poll 实现同时读取鼠标和键盘(非阻塞 I/O 方式)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define MOUSE "/dev/input/event3"
int main(void) {
char buf[100];
int fd, ret = 0, flag;
int loops = 5;
struct pollfd fds[2];
/* 打开鼠标设备文件 */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将键盘设置为非阻塞方式 */
flag = fcntl(0, F_GETFL); // 先获取原来的 flag
flag |= O_NONBLOCK; // 将 O_NONBLOCK 标准添加到 flag
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 error");
goto out;
}
else if (ret == 0) {
fprintf(stderr, "poll timeout.\n");
continue;
}
/* 检查键盘是否为就绪态 */
if (fds[0].revents & POLLIN) {
ret = read(0, buf, sizeof(buf));
if (ret > 0)
printf("键盘: 成功读取<%d>个字节数据\n", ret);
}
/* 检查鼠标是否为就绪态 */
if (fds[1].revents & POLLIN) {
ret = read(fd, buf, sizeof(buf));
if (ret > 0)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
}
}
out:
/* 关闭文件 */
close(fd);
exit(ret);
}
使用 select() 或 poll() 时需要注意一个问题
- 当监测到某一个或多个文件描述符成为就绪态(可读或写)时,需要执行相应的 I/O 操作以清除该状态,否则该状态会一直存在,那么下一次调用 select() 或 poll() 时,文件描述符已经处于就绪态了,将直接返回
在 I/O 多路复用中,进程通过系统调用 select() 或 poll() 来主动查询文件描述符上是否可以执行 I/O 操作
而异步 I/O 中,当文件描述符上可执行 I/O 操作时,进程可以请求内核为自己发送一个信号,之后进程就可执行任何其它的任务直到文件描述符可执行 I/O 操作为止,此时内核会发送信号给进程,异步 I/O 通常也称信号驱动 I/O
要使用异步 I/O,程序需按照如下步骤执行
int flag;
flag = fcntl(0, F_GETFL); // 先获取原来的 flag
flag |= O_ASYNC; // 将 O_ASYNC 标志添加到 flag
fcntl(fd, F_SETFL, flag); // 重新设置 flag
fcntl(fd, F_SETOWN, getpid());
#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/mouse2"
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(void) {
int flag;
/* 打开鼠标设备文件<使能非阻塞 I/O> */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (fd == -1) {
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);
}
}
在一个需要同时检查大量文件描述符(如数千个)的应用程序中,例如某种类型的网络服务端程序,与 select() 和 poll() 相比,异步 I/O 能够提供显著的性能优势
对于 select() 或 poll() 函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O 操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检查操作
默认异步 I/O 的缺陷
// 指定了 SIGRTMIN 实时信号作为文件描述符 fd 的异步 I/O 通知信号
// 如果第三个参数 arg 设置为 0,则表示指定 SIGIO 信号作为异步 I/O 通知信号
fcntl(fd, F_SETSIG, SIGRTMIN);
在应用程序中需要为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指定 SA_SIGINFO,表示使用 sa_sigaction 指向的函数作为信号处理函数,而不使用 sa_handler 指向的函数。因为 sa_sigaction 指向的函数作为信号处理函数提供了更多的参数,可以获取到更多信息
对于异步 I/O 事件而言,传递给信号处理函数的 siginfo_t 结构体中与之相关的字段如下
// 使用实时信号 + sigaction 优化异步 I/O
#define _GNU_SOURCE // 需要定义了_GNU_SOURCE 宏之后才能使用 F_SETSIG
#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/mouse2"
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 (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
loops--;
if (0 >= loops) {
close(fd);
exit(0);
}
}
}
int main(void) {
struct sigaction act;
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());
/* 指定实时信号 SIGRTMIN 作为异步 I/O 通知信号 */
fcntl(fd, F_SETSIG, SIGRTMIN);
/* 为实时信号 SIGRTMIN 注册信号处理函数 */
act.sa_sigaction = io_handler;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGRTMIN, &act, NULL);
for (;;)
sleep(1);
}
mmap() 用于告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
对指定映射区的保护要求不能超过文件 open() 时的访问权限,如:文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE
通过 mmap() 将文件映射到进程地址空间中的一块内存区域中,当不再需要时,使用 munmap() 解除映射关系
#include <sys/mman.h>
int munmap(void *addr, size_t length);
示例:使用存储映射 I/O 复制文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc, char* argv[]) {
int srcfd, dstfd;
void *srcaddr;
void *dstaddr;
int ret;
struct stat sbuf;
if (argc != 3) {
fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
exit(-1);
}
/* 打开源文件 */
srcfd = open(argv[1], O_RDONLY);
if (srcfd == -1) {
perror("open error");
exit(-1);
}
/* 打开目标文件 */
dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664);
if (dstfd == -1) {
perror("open error");
ret = -1;
goto out1;
}
/* 获取源文件的大小 */
fstat(srcfd, &sbuf);
/* 设置目标文件的大小 */
ftruncate(dstfd, sbuf.st_size);
/* 将源文件映射到内存区域中 */
// 将参数 prot 指定为 PROT_READ,表示对它的映射区会进行读取操作
srcaddr = mmap(NULL, sbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0);
if (srcaddr == MAP_FAILED) {
perror("mmap error");
ret = -1;
goto out2;
}
/* 将目标文件映射到内存区域中 */
// 将参数 port 指定为 PROT_WRITE,表示对它的映射区会进行写入操作
dstaddr = mmap(NULL, sbuf.st_size, PROT_WRITE, MAP_SHARED, dstfd, 0);
if (dstaddr == MAP_FAILED) {
perror("mmap error");
ret = -1;
goto out3;
}
/* 将源文件映射区中的内容复制到目标文件映射区中,完成文件复制操作 */
memcpy(dstaddr, srcaddr, sbuf.st_size);
/* 程序退出前清理工作 */
out4:
/* 解除目标文件映射 */
munmap(dstaddr, sbuf.st_size);
out3:
/* 解除源文件映射 */
munmap(srcaddr, sbuf.st_size);
out2:
/* 关闭目标文件 */
close(dstfd);
out1:
/* 关闭源文件并退出 */
close(srcfd);
exit(ret);
}
$ touch srcfile
$ vi srcfile
$ cat srcfile
Hello World!
Hunan normal university.
$ gcc copy.c -o copy
$ ./copy ./srcfile ./dstfile
$ cat dstfile
Hello World!
Hunan normal university.
#include <sys/mman.h>
// 参数 prot 的取值与 mmap() 函数一样,mprotect() 函数会将指定地址范围的保护要求更改为参数 prot 所指定的类型
// 参数 addr 指定该地址范围的起始地址,addr 的值必须是系统页大小的整数倍
// 参数 len 指定该地址范围的大小
int mprotect(void *addr, size_t len, int prot);
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
普通 I/O 方式的缺点
存储映射 I/O 的优点
通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()、write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存
多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O 操作时,容易产生竞争状态,导致文件中的内容与预想的不一致
建议性锁
强制性锁
使用 flock() 函数可以对文件加锁或者解锁,但只能产生建议性锁
#include <sys/file.h>
int flock(int fd, int operation);
示例 1:使用 flock() 对文件加锁/解锁
// test1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>
static int fd = -1; // 文件描述符
/* 信号处理函数 */
static void sigint_handler(int sig) {
if (sig != SIGINT) {
return;
}
/* 解锁 */
flock(fd, LOCK_UN);
close(fd);
printf("进程 1: 文件已解锁!\n");
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: %s<file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_WRONLY);
if (fd == -1) {
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(互斥锁) */
if (flock(fd, LOCK_EX | LOCK_NB)) {
perror("进程 1: 文件加锁失败");
exit(-1);
}
printf("进程 1: 文件加锁成功!\n");
/* 为 SIGINT 信号注册处理函数 */
// 当进程接收到 SIGINT 信号后会执行 sigint_handler() 函数
// 在信号处理函数中对文件进行解锁,然后终止进程
signal(SIGINT, sigint_handler);
for (;;) {
sleep(1);
}
}
示例 2:未获取锁情况下读写文件
// test2.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>
int main(int argc, char *argv[]) {
char buf[100] = "Hello World!";
int fd;
int len;
if (argc != 2) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 可读可写方式打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1) {
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(互斥锁) */
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
perror("进程 2: 文件加锁失败");
} else {
printf("进程 2: 文件加锁成功!\n");
}
/* 写文件 */
len = strlen(buf);
if (write(fd, buf, len) < 0) {
perror("write error");
exit(-1);
}
printf("进程 2: 写入到文件的字符串<%s>\n", buf);
/* 将文件读写位置移动到文件头 */
if (lseek(fd, 0x0, SEEK_SET) < 0) {
perror("lseek error");
exit(-1);
}
/* 读文件 */
memset(buf, 0x0, sizeof(buf)); //清理 buf
if (read(fd, buf, len) < 0) {
perror("read error");
exit(-1);
}
printf("进程 2: 从文件读取的字符串<%s>\n", buf);
/* 解锁、退出 */
flock(fd, LOCK_UN);
close(fd);
exit(0);
}
$ gcc test1.c -o test1
$ gcc test2.c -o test2
$ touch infile
# 首先执行 test1 应用程序,将 infile 文件作为输入文件,并将其放置在后台运行
$ ./test1 ./infile &
[1] 3234
进程 1: 文件加锁成功!
# test1 会在后台运行,由 ps 命令可查看到其 pid 为 3234
$ ps
PID TTY TIME CMD
3068 pts/0 00:00:00 bash
3234 pts/0 00:00:00 test1
3235 pts/0 00:00:00 ps
# 接着执行 test2 应用程序,传入相同的文件 infile
# test2 进程对 infile 文件加锁失败,原因在于锁已经被 test1 进程所持有
# 但是 test2 对文件的读写操作是没有问题的
$ ./test2 ./infile
进程 2: 文件加锁失败: Resource temporarily unavailable
进程 2: 写入到文件的字符串<Hello World!>
进程 2: 从文件读取的字符串<Hello World!>
# 接着向 test1 进程发送一个 SIGIO 信号(编号为 2),让其对文件 infile 解锁
$ kill -2 3234
进程 1: 文件已解锁!
# 接着再执行一次 test2
$ ./test2 ./infile
# test2 成功对 infile 文件加锁,读写也是没有问题的
$ ./test2 ./infile
进程 2: 文件加锁成功!
进程 2: 写入到文件的字符串<Hello World!>
进程 2: 从文件读取的字符串<Hello World!>
关于 flock() 的几条规则
- 1. 同一进程对文件多次加锁不会导致死锁
新加的锁会替换旧的锁,如:先调用 flock() 对文件加共享锁,然后再调用 flock() 对文件加互斥锁,最终文件锁会由共享锁替换为互斥锁- 2. 文件关闭时会自动解锁
文件锁会在相应的文件描述符被关闭之后自动释放(当一个进程终止时,它所建立的锁将全部释放)- 3. 一个进程不可以对另一个进程持有的文件锁进行解锁
- 4. 由 fork() 创建的子进程不会继承父进程所创建的锁
若一个进程对文件加锁成功,然后该进程调用 fork() 创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子进程从父进程继承了其文件描述符,但不能继承文件锁,因为锁的作用就是阻止多个进程同时写同一个文件- 5. 当一个文件描述符被复制时(使用 dup()、dup2() 或 fcntl() F_DUPFD 操作)
这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以
flock(fd, LOCK_EX); // 加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); // 解锁
// 如果不显示的调用一个解锁操作,只有当所有文件描述符都被关闭之后锁才会被释放
// 如果不调用 flock(new_fd, LOCK_UN) 进行解锁,只有当 fd 和 new_fd 都被关闭之后锁才会自动释放
fcntl() 实现文件锁功能与 flock() 的区别
#include <unistd.h>
#include <fcntl.h>
// 与锁相关的 cmd 为 F_SETLK、F_SETLKW、F_GETLK,第三个参数 flockptr 是一个 struct flock 结构体指针
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
struct flock 结构体
struct flock {
...
/* 锁类型,可设置为:F_RDLCK 表示共享性质的读锁,F_WRLCK 表示独占性质的写锁,F_UNLCK 表示解锁一个区域 */
short l_type;
/* l_whence 和 l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量 */
short l_whence; /* 插入 l_start 的位置: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* 需要加锁或解锁区域的字节长度 */
// 一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效
pid_t l_pid;
...
};
两种类型的锁:F_RDLCK(共享性读锁)和 F_WRLCK(独占性写锁)
文件锁相关的三个 cmd:F_SETLK、F_SETLKW 和 F_GETLK
示例 1:使用 fcntl() 对文件加锁/解锁
#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(int argc, char *argv[]) {
struct flock lock = {0};
int fd = -1;
char buf[] = "Hello World!";
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_WRONLY);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 对文件加锁 */
lock.l_type = F_WRLCK; // 独占性写锁
lock.l_whence = SEEK_SET; // 文件头部
lock.l_start = 0; // 偏移量为 0
lock.l_len = 0; // l_len 设置为 0 表示对整个文件加锁
if (-1 == fcntl(fd, F_SETLK, &lock)) {
perror("加锁失败");
exit(-1);
}
printf("对文件加锁成功!\n");
/* 对文件进行写操作 */
if (0 > write(fd, buf, strlen(buf))) {
perror("write error");
exit(-1);
}
/* 解锁 */
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
/* 退出 */
close(fd);
exit(0);
}
$ gcc fcntl.c -o fcntl
$ touch testfile
$ ./fcntl ./testfile
对文件加锁成功!
示例 2:使用 fcntl() 对文件不同区域进行加锁
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
struct flock wr_lock = {0};
struct flock rd_lock = {0};
int fd = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件大小截断为 1024 字节 */
ftruncate(fd, 1024);
/* 对 100~200 字节区间加写锁 */
wr_lock.l_type = F_WRLCK;
wr_lock.l_whence = SEEK_SET;
wr_lock.l_start = 100;
wr_lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &wr_lock)) {
perror("加写锁失败");
exit(-1);
}
printf("加写锁成功!\n");
/* 对 400~500 字节区间加读锁 */
rd_lock.l_type = F_RDLCK;
rd_lock.l_whence = SEEK_SET;
rd_lock.l_start = 400;
rd_lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &rd_lock)) {
perror("加读锁失败");
exit(-1);
}
printf("加读锁成功!\n");
/* 对文件进行 I/O 操作 */
// ......
// ......
/* 解锁 */
wr_lock.l_type = F_UNLCK; // 写锁解锁
fcntl(fd, F_SETLK, &wr_lock);
rd_lock.l_type = F_UNLCK; // 读锁解锁
fcntl(fd, F_SETLK, &rd_lock);
/* 退出 */
close(fd);
exit(0);
}
示例 3:读锁的共享性测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
struct flock lock = {0};
int fd = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件大小截断为 1024 字节 */
ftruncate(fd, 1024);
/* 对 400~500 字节区间加读锁 */
lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 400;
lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &lock)) {
perror("加读锁失败");
exit(-1);
}
printf("加读锁成功!\n");
for (;;)
sleep(1);
}
# 程序加读锁之后会进入死循环,进程一直在运行并持有读锁
# 多个进程对同一文件的相同区域都可以加读锁,说明读锁是共享性的
$ ./read ./testfile &
[1] 2771
加读锁成功!
$ ./read ./testfile &
[2] 2773
加读锁成功!
$ ./read ./testfile &
[3] 2774
加读锁成功!
...
示例 4:写锁的独占性测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
struct flock lock = {0};
int fd = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 将文件大小截断为 1024 字节 */
ftruncate(fd, 1024);
/* 对 400~500 字节区间加写锁 */
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 400;
lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &lock)) {
perror("加写锁失败");
exit(-1);
}
printf("加写锁成功!\n");
for (;;)
sleep(1);
}
# 第一次启动的进程对文件加写锁之后,后面再启动进程对同一文件的相同区域加写锁都会失败,因此写锁是独占性的
$ ./write ./testfile2 &
[5] 2812
加写锁成功!
$ ./write ./testfile2 &
[6] 2813
加写锁失败: Resource temporarily unavailable
...
使用 fcntl() 创建锁的几条规则
- 文件关闭的时候,会自动解锁
- 一个进程不可以对另一个进程持有的文件锁进行解锁
- 由 fork() 创建的子进程不会继承父进程所创建的锁
- 当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以