第七章 高级 I/O

第七章 高级 I/O

7.1 非阻塞 I/O

阻塞其实就是进入了休眠状态,交出了 CPU 控制权。前面所学习过的函数,譬如 wait()、pause()、sleep()等函数都会进入阻塞。
阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的I/O 操作是非阻塞的。这样说大家可能不太明白,这里举个例子,譬如对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O进行操作。

7.1.1 阻塞 I/O 与非阻塞 I/O 读文件

示例代码 演示了以阻塞方式读取鼠标,调用 open()函数打开鼠标设备文件"/dev/input/event3",以只读方式打开,没有指定 O_NONBLOCK 标志,说明使用的是阻塞式 I/O;程序中只调用了一次 read()读取鼠标。

#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/event3", O_RDONLY);
 if (-1 == fd) {
 perror("open error");
 exit(-1);
 }
 /* 读文件 */
 memset(buf, 0, sizeof(buf));
 ret = read(fd, buf, sizeof(buf));
 if (0 > ret) {
 perror("read error");
 close(fd);
 exit(-1);
 }
 printf("成功读取<%d>个字节数据\n", ret);
 /* 关闭文件 */
 close(fd);
 exit(0);
}

编译上述示例代码进行测试:
在这里插入图片描述
执行程序之后,发现程序没有立即结束,而是一直占用了终端,没有输出信息,原因在于调用 read()之后进入了阻塞状态,因为当前鼠标没有数据可读;如果此时我们移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read()会成功读取到数据并返回,如下所示:
在这里插入图片描述
接下来, 将示例代码修改成非阻塞式 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>
int main(void) {
 char buf[100];
 int fd, ret;
 /* 打开文件 */
 fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
 if (-1 == fd) {
 perror("open error");
 exit(-1);
 }
 /* 读文件 */
 memset(buf, 0, sizeof(buf));
 ret = read(fd, buf, sizeof(buf));
 if (0 > ret) {
 perror("read error");
 close(fd);
 exit(-1);
 }
 printf("成功读取<%d>个字节数据\n", ret);
 /* 关闭文件 */
 close(fd);
 exit(0);
}

修改方法很简单,只需在调用 open()函数时指定 O_NONBLOCK 标志即可,对上述示例代码进行编译测试:
在这里插入图片描述
执行程序之后,程序立马就结束了,并且调用 read()返回错误,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有发生输入事件),是没有数据可读,故而导致失败返回,这就是非阻塞 I/O。
可以对示例代码 进行修改,使用轮训方式不断地去读取,直到鼠标有数据可读,read()将会成功返回:

#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/event3", 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);
 }
 } }

具体的执行的效果便不再演示了,各位读者自己动手试试。

7.1.2 阻塞 I/O 的优点与缺点

当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!
所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!

7.1.3 使用非阻塞 I/O 实现并发读取

上一小节给大家所举的例子当中,只读取了鼠标的数据,如果要在程序当中同时读取鼠标和键盘,那又该如何呢?本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 同时读取鼠标和键盘;同理键盘也是一种输入类设备,但是键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。
首先我们使用阻塞式方式同时读取鼠标和键盘,示例代码如下所示:

#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/event3"
int main(void) {
 char buf[100];
 int fd, ret;
 /* 打开鼠标设备文件 */
 fd = open(MOUSE, O_RDONLY);
 if (-1 == fd) {
 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);
}

上述程序中先读了鼠标,在接着读键盘,所以由此可知,在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。
这就是阻塞式 I/O 的一个困境,无法实现并发读取(同时读取),主要原因在于阻塞,那如何解决这个问题呢?当然大家可能会想到使用多线程,一个线程读取鼠标、另一个线程读取键盘,亦或者创建一个子进程,父进程读取鼠标、子进程读取键盘等方法,当然这些方法自然可以解决,但不是我们要学习的重点。
既然阻塞 I/O 存在这样一个困境,那我们可以使用非阻塞式 I/O 解决它,将示例代码 13.1.4 修改为非阻塞式方式同时读取鼠标和键盘。使用 open()打开得到的文件描述符,调用 open()时指定 O_NONBLOCK 标志将其设置为非阻塞式 I/O;因为标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用 open()打开得到的,那如何将标准输入设置为非阻塞 I/O。可通过如下代码将标准输入(键盘)设置为非阻塞方式:

int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag
fcntl(0, F_SETFL, flag); //重新设置 flag

示例代码演示了以非阻塞方式同时读取鼠标和键盘。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define MOUSE "/dev/input/event3"
int main(void) {
 char buf[100];
 int fd, ret, flag;
 /* 打开鼠标设备文件 */
 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
 for ( ; ; ) {
 /* 读鼠标 */
 ret = read(fd, buf, sizeof(buf));
 if (0 < ret)
 printf("鼠标: 成功读取<%d>个字节数据\n", ret);
 /* 读键盘 */
 ret = read(0, buf, sizeof(buf));
 if (0 < ret)
 printf("键盘: 成功读取<%d>个字节数据\n", ret);
 }
 /* 关闭文件 */
 close(fd);
 exit(0);
}

将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码进行编译,测试结果:
在这里插入图片描述
这样就解决了上上示例代码所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数据。
虽然使用非阻塞 I/O 方式解决了上述示例代码 出现的问题,但由于程序当中使用轮训方式,故而会使得该程序的 CPU 占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用,如何解决这样的问题呢?我们将在下一小节向大家介绍。

7.2 I/O 多路复用

上一小节虽然使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题,但依然不够完美,使得程序的 CPU 占用率特别高。解决这个问题,就要用到本小节将要介绍的 I/O 多路复用方法。

7.2.1 何为 I/O 多路复用

I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
由此可知,I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。
我们可以采用系统调用poll()来执行 I/O 多路复用操作,
I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路 I/O。

7.2.2 poll()函数介绍

在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。poll()函数原型如下所示:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

函数参数含义如下:
fds:指向一个 struct pollfd 类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件。
nfds:参数 nfds 指定了 fds 数组中的元素个数
timeout:该参数与 select()函数的 timeout 参数相似,用于决定 poll()函数的阻塞行为,具体用法如下:
⚫ 如果 timeout 等于-1,则 poll()会一直阻塞(与 select()函数的 timeout 等于 NULL 相同),直到 fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
⚫ 如果 timeout 等于 0,poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
⚫ 如果 timeout 大于 0,则表示设置 poll()函数阻塞时间的上限值,意味着 poll()函数最多阻塞 timeout毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
struct pollfd 结构体如下所示:

struct pollfd {
 int fd; /* file descriptor */
 short events; /* requested events */
 short revents; /* returned events */
};

fd 是一个文件描述符,struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件。当 poll()函数返回时,revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件(注意,poll()没有更改 events 变量),我们可以对 revents 进行检查,判断文件描述符 fd 发生了什么事件。
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 (0 > ret) {
 perror("poll error");
 goto out;
 }
 else if (0 == ret) {
 fprintf(stderr, "poll timeout.\n");
 continue;
 }
 /* 检查键盘是否为就绪态 */
 if(fds[0].revents & POLLIN) {
 ret = read(0, buf, sizeof(buf));
 if (0 < ret)
 printf("键盘: 成功读取<%d>个字节数据\n", ret);
 }
 /* 检查鼠标是否为就绪态 */
 if(fds[1].revents & POLLIN) {
 ret = read(fd, buf, sizeof(buf));
 if (0 < ret)
 printf("鼠标: 成功读取<%d>个字节数据\n", ret);
 }
 }
out:
 /* 关闭文件 */
 close(fd);
 exit(ret);
}

在这里插入图片描述

7.3 异步 IO

在 I/O 多路复用中,进程通过系统调用 poll()来主动查询文件描述符上是否可以执行 I/O 操作。而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。所以要使用异步 I/O,还得结合前面所学习的信号相关的内容,所以异步 I/O 通常也称为信号驱动 I/O。
要使用异步 I/O,程序需要按照如下步骤来执行:
⚫ 通过指定 O_NONBLOCK 标志使能非阻塞 I/O。
⚫ 通过指定 O_ASYNC 标志使能异步 I/O。
⚫ 设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程,通常将调用进程设置为异步 I/O 事件的接收进程。
⚫ 为内核发送的通知信号注册一个信号处理函数。默认情况下,异步 I/O 的通知信号是 SIGIO,所以内核会给进程发送信号 SIGIO。
⚫ 以上步骤完成之后,进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进行 I/O 操作。
需要注意的是:在调用 open()时无法通过指定 O_ASYNC 标志来使能异步 I/O,但可以使用 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 操作。
使用示例
示例代码 演示了以异步 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);
}

代码比较简单,这里我们进行编译测试:
在这里插入图片描述

7.4 存储映射 I/O

存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read()和 write()情况下执行 I/O 操作。

7.4.1 mmap()和 munmap()函数

为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现。其函数原型如下所示:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:参数 addr 用于指定映射到内存区域的起始地址。通常将其设置为 NULL
length:参数 length 指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位,譬如length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。
offset:文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射;所以参数 offset 和参数 length就确定了文件的起始位置和长度。
fd:文件描述符,指定要映射到内存区域中的文件。
prot:参数 prot 指定了映射区的保护要求,可取值如下:
⚫ PROT_EXEC:映射区可执行;
⚫ PROT_READ:映射区可读;
⚫ PROT_WRITE:映射区可写;
⚫ PROT_NONE:映射区不可访问。
可将 prot 指定为为 PROT_NONE,也可将其设置为 PROT_EXEC、PROT_READ、PROT_WRITE 中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限,譬
如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。
flags:参数 flags 可影响映射区的多种属性,一般设置为MAP_SHARED
munmap()解除映射
通过 open()打开文件,需要使用 close()将将其关闭;同理,通过 mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射,使用 munmap()解除映射关系,其函数原型如下所示:

#include <sys/mman.h>
int munmap(void *addr, size_t length);

使用示例
通过以上介绍,接下来我们编写一个简单地示例代码,使用存储映射 I/O 进行文件复制。示例代码演示了使用存储映射 I/O 实现文件复制操作,将源文件中的内容全部复制到另一个目标文件中,其效果类似于 cp 命令。

#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 (3 != argc) {
 fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
 exit(-1);
 }
 /* 打开源文件 */
 srcfd = open(argv[1], O_RDONLY);
 if (-1 == srcfd) {
 perror("open error");
 exit(-1);
 }
 /* 打开目标文件 */
 dstfd = open(argv[2], O_RDWR |
 O_CREAT | O_TRUNC, 0664);
 if (-1 == dstfd) {
 perror("open error");
 ret = -1;
 goto out1;
 }
 /* 获取源文件的大小 */
 fstat(srcfd, &sbuf);
 /* 设置目标文件的大小 */
 ftruncate(dstfd, sbuf.st_size);
 /* 将源文件映射到内存区域中 */
 srcaddr = mmap(NULL, sbuf.st_size,
 PROT_READ, MAP_SHARED, srcfd, 0);
 if (MAP_FAILED == srcaddr) {
 perror("mmap error");
 ret = -1;
 goto out2;
 }
 /* 将目标文件映射到内存区域中 */
 dstaddr = mmap(NULL, sbuf.st_size,
 PROT_WRITE, MAP_SHARED, dstfd, 0);
 if (MAP_FAILED == dstaddr) {
 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);
}

当执行程序的时候,将源文件和目标文件传递给应用程序,该程序首先会将源文件和目标文件打开,源文件以只读方式打开,而目标文件以可读、可写方式打开,如果目标文件不存在则创建它,并且将文件的大小截断为 0。
然后使用 fstat()函数获取源文件的大小,接着调用 ftruncate()函数设置目标文件的大小与源文件大小保持一致。
然后对源文件和目标文件分别调用 mmap(),将文件映射到内存当中;对于源文件,调用 mmap()时将参数 prot 指定为 PROT_READ,表示对它的映射区会进行读取操作;对于目标文件,调用 mmap()时将参数 port指定为 PROT_WRITE,表示对它的映射区会进行写入操作。最后调用 memcpy()将源文件映射区中的内容复制到目标文件映射区中,完成文件的复制操作。
接下来我们进行测试,笔者使用当前目录下的 srcfile 作为源文件,dstfile 作为目标文件,先看看源文件srcfile 的内容,如下所示:
在这里插入图片描述
目标文件 dstfile 并不存在,我们需要在程序中进行创建,编译程序、运行:
在这里插入图片描述
由打印信息可知,程序运行完之后,生成了目标文件 dstfile,使用 cat 命令查看到其内容与源文件 srcfile相同,本测试程序成功实现了文件复制功能!

7.4.2 msync()函数

read()和 write()系统调用在操作磁盘文件时不会直接发起磁盘访问(读写磁盘硬件),而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,在后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘中,所以由此可知,调用 write()写入到磁盘文件中的数据并不会立马写入磁盘,而是会
先缓存在内核缓冲区中,所以就会出现 write()操作与磁盘操作并不同步,也就是数据不同步。
对于存储 I/O 来说亦是如此,写入到文件映射区中的数据也不会立马刷新至磁盘设备中,而是会在我们将数据写入到映射区之后的某个时刻将映射区中的数据写入磁盘中。所以会导致映射区中的内容与磁盘文件中的内容不同步。我们可以调用 msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作),系统调用 msync()类似于 fsync()函数,不过 msync()作用于映射区。该函数原型如下所示:

#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);

参数 addr 和 length 指定了需同步的内存区域的起始地址和大小。调用 msync()时,将 addr 设置为 mmap()函数的返回值,将 length 设置为 mmap()函数的 length 参数,将对文件的整个映射区进行同步操作。参数 flags 应指定为 MS_ASYNC 和 MS_SYNC 两个标志之一。

7.4.3 普通 I/O 与存储映射 I/O 比较

普通 I/O 方式的缺点
普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写,使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。同样使用标准 I/O(库函数 fread()、fwrite())也是如此,本身标准 I/O 就是对普通 I/O 的一种封装。那既然效率较低,为啥还要使用这种方式呢?原因在于,只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的 I/O 方式还是非常方便的。
存储映射 I/O 的优点
在效率上会比普通 I/O 要高,其次上面也讲了,普通 I/O 中间涉及到了很多的函数调用过程,这些都会导致普通 I/O 在效率上会比存储映射 I/O 要低。
前面提到存储映射 I/O 的实质其实是共享,如何理解共享呢?其实非常简单,我们知道,应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操作硬件。通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()、write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。
存储映射 I/O 的不足
存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的区域已经在调用mmap()函数时通过 length 参数指定了。使用存储映射 I/O 在进行大数据量操作时比较有效;对于少量数据,使用普通 I/O 方式更加方便!
存储映射 I/O 的应用场景
存储映射 I/O 在处理大量数据时效率高,对于少量数据处理不是很划算,所以通常来说,存储映射 I/O 会在视频图像处理方面用的比较多,譬如在第二篇内容,我们将会介绍 Framebuffer 编程,通俗点说就是 LCD 编程,就会使用到存储映射 I/O。

7.5 文件锁

现象一下,当两个人同时编辑磁盘中同一份文件时,其后果将会如何呢?在 Linux 系统中,该文件的最后状态通常取决于写该文件的最后一个进程。多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O 操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!
对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。
文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性,linux 通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。
一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态。
flock()函数加锁
先来学习系统调用 flock(),使用该函数可以对文件加锁或者解锁,其函数原型如下所示:

#include <sys/file.h>
int flock(int fd, int operation);

函数参数和返回值含义如下:
fd:参数 fd 为文件描述符,指定需要加锁的文件。
operation:参数 operation 指定了操作方式,可以设置为以下值的其中一个:
⚫ LOCK_SH:在 fd 引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一个文件的共享锁,该共享锁可被多个进程同时拥有。
⚫ LOCK_EX:在 fd 引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只能同时被一个进程所拥有。
⚫ LOCK_UN:解除文件锁定状态,解锁、释放锁。除了以上三个标志外,还有一个标志:
⚫ LOCK_NB:表示以非阻塞方式获取锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值