Linux-笔记 高级I/O操作

目录

一、前言

二、阻塞I/O与非阻塞I/O

三、I/O多路复用

1、slect机制

2、poll机制

总结

四、异步I/O

1、执行步骤

2、O_ASYNC 标志

3、设置异步 I/O 事件的接收进程

4、注册 SIGIO 信号的处理函数

5、实操

6、展望与优化

五、储存映射I/O

1、实现

1.1 映射函数mmap

1.2 解除映射函数munmap

1.3 实操

2、总结

2.1 映射 I/O 与普通I/O区别

2.2 应用场景

2.3 不足之处

六、文件锁

1、概念

2、分类

3、上锁函数

3.1 flock( )

3.1.1 规则

3.1.2 实操

3.2 fcntl( )

3.2.1 规则

3.2.2 实操   

整个文件加锁/解锁

文件不同区域加锁/解锁

验证读锁的共享性

验证写锁的独占性

七、总结


一、前言

        I/O(Input/Output,输入/输出)是计算机系统中的一个重要组成部分,它是指计算机与 外部世界之间的信息交流过程。I/O 操作是计算机系统中的一种基本操作,用于向外部设备(如 硬盘、键盘、鼠标、网络等)读取数据或向外部设备写入数据。

        常见的I/O操作方式:

        1)同步 I/O(Synchronous I/O):在进行 I/O 操作时,程序会一直等待操作完成后再 继续执行后面的代码。如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。

        2)异步 I/O(Asynchronous I/O):在进行 I/O 操作时,程序会立即返回,而不必等待 操作完成。当操作完成后,操作系统会通知程序。这种方式可以允许程序在等待 I/O 操作完 成的同时执行其他代码。

        3)阻塞 I/O(Blocking I/O):在进行 I/O 操作时,程序会一直等待操作完成后再继续执行后面的代码。如果 I/O 操作阻塞,程序会一直等待,直到操作完成或超时。阻塞 I/O 是 同步 I/O 的一种。

        4)非阻塞 I/O(Non-blocking I/O):在进行 I/O 操作时,程序会立即返回,而不必等待操作完成。如果 I/O 操作无法立即完成,程序也会立即返回,但是会周期性地检查操作是否完成。非阻塞 I/O 是同步 I/O 的一种。

        5)I/O 多路复用(I/O Multiplexing):是一种同时监视多个 I/O 事件的机制,通常使用select、poll、epoll 等系统调用。程序通过这些调用告知操作系统它要监视哪些 I/O 事件,当有 I/O 事件发生时,操作系统通知程序,并返回发生事件的描述符。I/O 多路复用通常是异步 I/O 模型的一部分。

二、阻塞I/O与非阻塞I/O

        阻塞和非阻塞的主要区别在于程序在进行 I/O 操作时是否会被阻塞。在实际应用中,阻塞 I/O 的使用场景较为有限,因为阻塞 I/O 会导致程序性能下降,会造成资源浪费。非阻塞 I/O 则可以较好地解决这个问题,但需要程序周期性地检查 I/O 操作是否完成,增加了编程难度。

接下来通过几个小实验来区分阻塞I/O与非阻塞I/O的区别。

       1)阻塞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>

int main(void) 
{
    char buf[1024];
    int fd, ret;

    fd = open("/dev/input/event2", O_RDONLY);
    if (-1 == fd) {
        perror("open error \r\n");
        exit(-1);
    }

    memset(buf, 0, sizeof(buf));
    ret = read(fd, buf, sizeof(buf));
    if (0 > ret) {
        perror("read error \r\n");
        close(fd);
        exit(-1);
    }

    printf("读到:%d\r\n", ret);

    close(fd);
    exit(0);
}

        2)非阻塞I/O读取鼠标数据,发现运行后立刻结束了程序,并输出了一些错误信息,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 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[1024];
    int fd, ret;

    fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
    if (-1 == fd) {
        perror("open error \r\n");
        exit(-1);
    }

    memset(buf, 0, sizeof(buf));
    ret = read(fd, buf, sizeof(buf));
    if (0 > ret) {
        perror("read error \r\n");
        close(fd);
        exit(-1);
    }

    printf("读到:%d\r\n", ret);

    close(fd);
    exit(0);
}

                

        3)通过非阻塞I/O+轮询读取鼠标数据,可以发现采用非阻塞方式也会停留住,等到移动鼠标才退出程序,这样虽然可行但是会占用很高的CPU使用率。

#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[1024];
    int fd, ret;

    fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error \r\n");
        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);
        }
    }
    printf("读到:%d\r\n", ret);

    close(fd);
    exit(0);
}

        4)通过对比发现阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU 资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致 该程序占用了非常高的 CPU 使用率!

        但是阻塞I/O也有缺点,我们都知道使用阻塞I/O会在没获取到鼠标移动数据的时候会阻塞,那么如果我在读取鼠标数据之后还有很多任务呢,那我一直不移动鼠标后面的任务就不用完成了吗,肯定不是。虽然我们可以通过创建多个线程去解决这个问题,但是也可以尝试其他办法,比如使用fcntl函数。

        5)使用fcntl函数将其它事件改为非阻塞就可以解决问题(fd为其他描述符)。

        int flag; flag = fcntl(fd, F_GETFL); //先获取原来的

        flag flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag

        fcntl(fd, F_SETFL, flag); //重新设置 flag

三、I/O多路复用

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

        I/O多路复用(I/O multiplexing)是一种在计算机系统中同时处理多个输入/输出(I/O)操作的技术。它允许程序在一个线程中高效地监控多个文件描述符,以便在任何一个文件描述符准备好进行I/O操作时立即响应,特别是在需要处理大量并发连接的情况下。

I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路 I/O。

这里介绍两个I/O多路复用的机制:select与poll机制。

1、slect机制

        系统调用 select()可用于执行 I/O 多路复用操作,调用 select()会一直阻塞,直到某一个或多个文件描述 符成为就绪态(可以读或写)。

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数:
nfds:监听的文件描述符集合中最大的文件描述符加1。通常可以使用 max_fd + 1 来设置。
readfds:指向一个 fd_set 结构,该结构中的文件描述符在 select 返回时如果可读则被置位。可以为 NULL。
writefds:指向一个 fd_set 结构,该结构中的文件描述符在 select 返回时如果可写则被置位。可以为 NULL。
exceptfds:指向一个 fd_set 结构,该结构中的文件描述符在 select 返回时如果有异常则被置位。可以为 NULL。
timeout:指定 select 的超时时间。如果为 NULL,则 select 将一直阻塞直到有文件描述符准备好。可以指定超时时间的精度到微秒级。

返回值:
返回值是准备好的文件描述符数量。如果返回值为0,表示超时。如果返回值为-1,表示出现错误。

fd_set 相关的宏:
FD_ZERO(fd_set *set):将 fd_set 结构清零。
FD_SET(int fd, fd_set *set):将文件描述符加入到 fd_set 结构中。
FD_CLR(int fd, fd_set *set):从 fd_set 结构中移除文件描述符。
FD_ISSET(int fd, fd_set *set):检查文件描述符是否在 fd_set 结构中。

        修改程序,在这个程序中select()函数的参数 timeout 被设置为 NULL,并且我们只关心鼠标或键盘是否有数据可读, 所以将参数 writefds 和 exceptfds 也设置为 NULL。执行 select()函数时,如果鼠标和键盘均无数据可读,则 select()调用会陷入阻塞,直到发生输入事件(鼠标移动、键盘上的按键按下或松开)才会返回。

#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[1024];
    int fd, fd2, ret, flag;
    fd_set rdfds;

    fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error \r\n");
        exit(-1);
    }
    
    fd2 = open("/dev/input/event1", O_RDONLY );
    if (-1 == fd2)
    {
        perror("open error \r\n");
        exit(-1);
    }

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

    /* 同时读取键盘和鼠标 */
    while (1) {
        FD_ZERO(&rdfds);
        FD_SET(fd2, &rdfds); //添加键盘
        FD_SET(fd, &rdfds);  //添加鼠标
        ret = select(fd + 2, &rdfds, NULL, NULL, NULL);
        if (0 > ret) {
            perror("select error");
            goto out;
        }
        else if (0 == ret) {
            fprintf(stderr, "select timeout.\n");
            continue;
        }

        /* 检查键盘是否为就绪态 */
        if(FD_ISSET(fd2, &rdfds)) {
            ret = read(fd2, buf, sizeof(buf));
            if (0 < ret)
            printf("键盘: 成功读取<%d>个字节数据\n", ret);
        }

        /* 检查鼠标是否为就绪态 */
        if(FD_ISSET(fd, &rdfds)) {
            ret = read(fd, buf, sizeof(buf));
            if (0 < ret)
            printf("鼠标: 成功读取<%d>个字节数据\n", ret);
        }
    }
out:
    /* 关闭文件 */
    close(fd);
    exit(ret);
}

         

2、poll机制

  poll 函数是Unix/Linux操作系统中用于实现I/O多路复用的一种机制,类似于 select,但在某些方面更加灵活和高效。它允许程序同时监控多个文件描述符的状态,并在这些文件描述符中任何一个准备好进行I/O操作时,通知程序进行处理。

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds:指向一个 struct pollfd 数组的指针,每个元素描述一个待检测的文件描述符及其关注的事件。
struct pollfd {
    int   fd;        /* 文件描述符 */
    short events;    /* 要监视的事件 */
    short revents;   /* 实际发生了的事件 */
};
fd:要监视的文件描述符。
events:要监视的事件,可以是 POLLIN(可读)、POLLOUT(可写)等。
revents:实际发生了的事件,由内核填充。

nfds:fds 数组中结构体的数量。

timeout:超时时间,单位是毫秒;如果为 -1,表示永久阻塞,直到有事件发生;如果为 0,表示立即返回,检查并返回当前就绪的文件描述符,如果大于 0,则表示等待的毫秒数。

返回值是发生事件的文件描述符数量。如果返回值为 0,表示超时;如果返回值为 -1,表示出现错误。

        使用 poll()函数来实现 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>
#include <poll.h>

int main(void)
{
    char buf[1024];
    int fd, fd2, ret, flag;
    struct pollfd fds[2];

    fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
    if (-1 == fd)
    {
        perror("open error \r\n");
        exit(-1);
    }
    
    fd2 = open("/dev/input/event1", O_RDONLY );
    if (-1 == fd2)
    {
        perror("open error \r\n");
        exit(-1);
    }

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

    /* 同时读取键盘和鼠标 */
    fds[0].fd = fd2;
    fds[0].events = POLLIN; //只关心数据可读
    fds[0].revents = 0;
    fds[1].fd = fd;
    fds[1].events = POLLIN; //只关心数据可读
    fds[1].revents = 0;
    /* 同时读取键盘和鼠标 */
    while (1) {

        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(fd2, 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);
    close(fd2);
    exit(ret);
}

                        

总结

        对于 select()或 poll()函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O 操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检查操作。当需要检查的文件描述符并不是很多时,使用 select()或 poll()是一种非常不错的方案!

        在使用 select()或 poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或 写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在;譬如select机制的代码中,调用 select()函数监测鼠标和键盘这两个文件描述符,当 select()返回时,通过 FD_ISSET()宏判断文件描述符上 是否可执行 I/O 操作;如果可以执行 I/O 操作时,应在应用程序中对该文件描述符执行 I/O 操作,以清除文 件描述符的就绪态,如果不清除就绪态,那么该状态将会一直存在,那么下一次调用 select()时,文件描述 符已经处于就绪态了,将直接返回。 同理对于 poll()函数来说亦是如此,当 poll()成功返回时,检查文件描述符是否称 为就绪态,如果文件描述符上可执行 I/O 操作时,也需要对文件描述符执行 I/O 操作,以清除就绪状态。

四、异步I/O

        异步I/O(Asynchronous I/O)是一种I/O操作模式,它允许程序在发起I/O操作后立即继续执行,而不必等待I/O操作完成。这种方式可以提高程序的效率和响应速度,特别是在处理大量I/O请求时。

        异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程 就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程,异步 I/O 通常也称为信号驱动 I/O。

1、执行步骤

1)指定 O_NONBLOCK 标志使能非阻塞 I/O。

2)指定 O_ASYNC 标志使能异步 I/O。

3)设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程, 通常将调用进程设置为异步 I/O 事件的接收进程。

4)为内核发送的通知信号注册一个信号处理函数。默认情况下,异步 I/O 的通知信号是 SIGIO,所以 内核会给进程发送信号 SIGIO。

       完成以上步骤后进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO 信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进 行 I/O 操作。

2、O_ASYNC 标志

        O_ASYNC 标志可用于使能文件描述符的异步 I/O 事件,当文件描述符可执行 I/O 操作时,内核会向异 步 I/O 事件的接收进程发送 SIGIO信号,在调用 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

3、设置异步 I/O 事件的接收进程

        为文件描述符设置异步 I/O 事件的接收进程,也可以通过 fcntl()函数 进行设置,将 cmd 设置为 F_SETOWN,第三个参数传入接收进程的进程 ID(PID),通常将调用进 程的 PID 传入:

fcntl(fd, F_SETOWN, getpid());

4、注册 SIGIO 信号的处理函数

        通过 signal()或 sigaction()函数为 SIGIO 信号注册一个信号处理函数,当进程接收到内核发送过来的 SIGIO 信号时,会执行该处理函数,在处理函数内去执行相应的 I/O 操作。

5、实操

使用异步I/O实现读鼠标应用程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

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;

    
    fd = open("/dev/input/event2", 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);
}

6、展望与优化

虽然实操代码能够实现功能但是存在以下两个问题:

1)默认情况下,异步I/O的通知信号SIGIO属于非排队信号。作为标准信号(即非实时、不可靠信号),SIGIO不支持信号排队机制。例如,当一个SIGIO信号的处理函数正在执行时,如果内核再次发送多个SIGIO信号给进程,这些信号将会被阻塞。只有当前信号处理函数执行完毕后,新的信号才会传递给进程,并且只能传递一次,其他后续的信号将会丢失。

2)无法确定文件描述符发生的具体事件。在实际代码中的信号处理函数sigio_handler()中,直接调用了read()函数来读取鼠标输入,但并未判断文件描述符是否处于可读就绪状态。事实上,这种异步I/O方式并不会告知应用程序文件描述符上发生了什么事件,即是否可读取、可写入或发生异常等。

所以可以优化一下:

1)使用实时信号替换默认信号 SIGIO,实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个 信号都能被接收,实时信号是 POSIX 标准的一部分,可用于应用进程。

fcntl(fd, F_SETSIG, SIGRTMIN);

2)使用 sigaction()函数注册信号处理函数

        在应用程序当中需要为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指 定 SA_SIGINFO,表示使用 sa_sigaction 指向的函数作为信号处理函数,而不使用 sa_handler 指向的函数。 因为 sa_sigaction 指向的函数作为信号处理函数提供了更多的参数,可以获取到更多信息。

        函数参数中包括一个 siginfo_t 指针,指向 siginfo_t 类型对象,当触发信号时该对象由内核构建。siginfo_t 结构体中提供了很多信息,我们可以在信号处理函数中使用这些信息:

1)si_signo:引发处理函数被调用的信号。这个值与信号处理函数的第一个参数一致

2)si_fd:表示发生异步 I/O 事件的文件描述符;

3)si_code:表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或者是异常事件等。

4)si_band:是一个位掩码,其中包含的值与系统调用 poll()中返回的 revents 字段中的值相同。

        在信号处理函数中通过对比 siginfo_t 结构体的 si_code 变量来检查文件描述符发生了什么事件,以采取相应的 I/O 操作。

si_codesi_band 掩码值描述/说明
POLL_INPOLLIN | POLLRDNORM可读取数据
POLL_OUTPOLLOUT | POLLWRNORM | POLLWRBAND可写入数据
POLL_MSGPOLLOUT | POLLWRNORM | POLLWRBAND不使用
POLL_ERRPOLLERRI/O 错误
POLL_PRIPOLLPRI | POLLRDNORM可读取高优先级数据
POLL_HUPPOLLHUP | POLLERR出现宕机

优化后:

注:程序最上面要定义宏,不然会报错

#define _GNU_SOURCE //在源文件开头定义_GNU_SOURCE 宏

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

static int fd;

static void newio_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;

    
    fd = open("/dev/input/event2", 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 = newio_handler;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    sigaction(SIGRTMIN, &act, NULL);

    for ( ; ; )
        sleep(1);
}

五、储存映射I/O

       这是一种将文件或设备映射到内存地址空间的机制,它可以讲文件映射到进程中的一段内存空间,当从这段内存空间读取与写入数据的时候,相当于从文件读取与写入。它使得应用程序可以通过指针直接访问文件或设备内容。通过这种方式,程序可以像操作内存一样进行读写操作,而无需通过系统调用。这种机制可以提高I/O操作的效率,尤其是在处理大量数据时。 

1、实现

1.1 映射函数mmap

        为了实现存储映射 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,这表示由系统选择该
    映射区的起始地址,这是最常见的设置方式;如果参数 addr 不为 NULL,则表示由自己指定映射区的起始
    地址,此函数的返回值是该映射区的起始地址。

length:
    参数 length 指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位,譬如
    length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。

offset:
    文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射;所以参数 offset 和参数 length
    就确定了文件的起始位置和长度,将文件的这部分映射到内存区域中,

fd:文件描述符,指定要映射到内存区域中的文件。

prot:参数 prot 指定了映射区的保护要求,可取值如下:
    PROT_EXEC:映射区可执行;
    PROT_READ:映射区可读;
    PROT_WRITE:映射区可写;
    PROT_NONE:映射区不可访问。

flags:参数 flags 可影响映射区的多种属性,参数 flags 必须要指定以下两种标志之一:
    MAP_SHARED:
    此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映
    射区中的数据更新到文件中,并且允许其它进程共享。
    MAP_PRIVATE:
    此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-onwrite),对映射区的任何                        
    操作都不会更新到文件中,仅仅只是对文件副本进行读写。

返回值:
    成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,通常使用
    MAP_FAILED 来表示,并且会设置 errno 来指示错误原因。

        对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,如下所示(以字节为单位): sysconf(_SC_PAGE_SIZE) 或 sysconf(_SC_PAGESIZE)。

需要注意的是:

1)当使用 mmap() 映射文件时,即使 length 参数不是页大小的整数倍,映射区的实际大小通常会是页大小的整数倍。比如,如果系统页大小为 4096 字节(4K),而文件大小为 96 字节,那么即使 length 参数设置为 96,系统也会分配 4096 字节的映射区。

2)文件内容会被映射到前 96 字节,剩余的 4000 字节(假设系统页大小为 4096 字节)会被填充为 0。修改这些额外的字节(4000 字节部分)不会影响到文件内容。

3)如果访问映射区之外的内存(即超过 4096 字节的部分),会导致异常情况,产生 SIGBUS 信号。

4)length 参数不能超过文件大小,即文件被映射的部分不能超出文件本身的大小。

与映射区相关的两个信号:

1)SIGSEGV:如果映射区被 mmap()指定成了只读的,那么进程试图将数据写入到该映射区时,将会 产生 SIGSEGV 信号,此信号由内核发送给进程,该信号的系统 默认操作是终止进程、并生成核心可用于调试的核心转储文件。

2)SIGBUS:如果映射区的某个部分在访问时已不存在,则会产生 SIGBUS 信号,该信号的系统默认操作是终止进程、并生成 核心可用于调试的核心转储文件。

1.2 解除映射函数munmap

        当不再需要使用映射区域,必须解除映射,使用 munmap()解除映射关系。

#include <sys/mman.h>
int munmap(void *addr, size_t length);
addr:
    指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;
length :
    一个非负整数,指定了待解除映射区域的大小(字节数),被解除映射的区域对应的大小也必须是系统页大        
    小的整数倍,即使参数 length 并不等于系统页大小的整数倍。

        需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close() 关闭文件时并不会解除映射。 通常将参数 addr 设置为 mmap()函数的返回值,将参数 length设置为 mmap()函数的参数 length,表示解除整个由 mmap()函数所创建的映射。

1.3 实操
#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);
}

2、总结

2.1 映射 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 的实质其实是共享,与 IPC 之内存共享很相似。共享的含义是:应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操 作硬件。通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()、write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。

        

2.2 应用场景

        存储映射 I/O 在处理大量数据时效率高,在多媒体处理、IPC、协同计算等场景适用。

2.3 不足之处

        存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小,因为在调用 mmap() 函数时,映射的区域已经通过 length 参数确定了大小。此外,文件映射的内存区域的大小必须是系统页大小的整数倍。例如,如果映射文件的大小是 96 字节,而系统页大小为 4096 字节,那么剩余的 4000 字节将全部填充为 0。虽然这些填充的字节可以通过映射地址访问,但它们不会反映在实际文件中。因此,存储映射 I/O 在处理大数据量操作时非常有效;但是,对于少量数据,使用普通 I/O 方式可能更加方便。

六、文件锁

1、概念

        文件锁是一种机制,用于协调多个进程对同一个文件的并发访问。它可以防止多个进程同时修改文件而导致数据不一致或冲突。

2、分类

根据锁的类型可以分为:

  • 共享锁(Shared Lock,读锁)

    • 允许多个进程同时持有,适用于多个进程并发读取文件。
    • 阻止其他进程获取独占锁。
  • 独占锁(Exclusive Lock,写锁)

    • 只允许一个进程持有,适用于一个进程独占修改文件。
    • 阻止其他进程获取共享锁或独占锁。

根据锁定机制可以分为:

  • 建议性锁(Advisory Locking)

    • 自愿参与,进程需要主动检查和设置锁。
    • 不强制执行,需要进程遵循协议。
    • 通过 flockfcntl 实现。
  • 强制性锁(Mandatory Locking)

    • 由操作系统强制执行,无需进程主动参与。
    • 确保所有进程遵守锁定协议。
    • 需要文件系统支持,并且文件需要特定权限设置。

根据锁定范围可以分为:

  • 整个文件锁(Whole File Lock)

    • 锁定整个文件,适用于对文件整体的读写控制。
    • 通常使用 flock 实现。
  • 文件区域锁(Byte-Range Lock)

    • 锁定文件的特定区域(字节范围),适用于部分文件的并发控制。
    • 通常使用 fcntl 实现。

3、上锁函数

3.1 flock( )

       系统调用 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:
    表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、直
    到其它进程释放锁为止,如果不想让程序被阻塞,可以指定 LOCK_NB 标志,如果无法获取到锁
    应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK),通常与 LOCK_SH 或 LOCK_EX
    一起使用,通过位或运算符组合在一起。
返回值:成功将返回 0;失败返回-1、并会设置 errno,
3.1.1 规则

1)同一进程对文件多次加锁不会导致死锁。

当一个进程对文件上锁后,另一个进程可以再次上锁,新加的锁会替换旧的锁。

2)文件关闭的时候,会自动解锁。

文件锁会在相应的文件描述符被关闭之后自动释放。同理,当一 个进程终止时,它所建立的锁将全部释放。

3)一个进程不可以对另一个进程持有的文件锁进行解锁。

4)由 fork()创建的子进程不会继承父进程所创建的锁。

若一个进程对文件加锁成功,然后 该进程调用 fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子 进程从父进程继承了其文件描述符,但不能继承文件锁。这个约束是有道理的,因为锁的作用就是 阻止多个进程同时写同一个文件,如果子进程通过 fork()继承了父进程的锁,则父进程和子进程就 可以同时写同一个文件了。

5)当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过 复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以。

flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁

3.1.2 实操

        通过写两个应用程序才上手,程序A先对一个测试文件上锁,然后一直死循环等待我们发送信号去解锁,程序B可以作为验证程序,验证测试文件是否已经被解锁了,如果解锁了,程序B就对其上锁。

程序A:

#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 (SIGINT != sig)
    return;
    /* 解锁 */
    flock(fd, LOCK_UN);
    close(fd);
    printf("进程 1: 文件已解锁!\n");
}

int main(int argc, char *argv[])
{
    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);
    }

    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
        perror("进程 1: 文件加锁失败");
        exit(-1);
    }

    printf("进程 1: 文件加锁成功!\n");
    
    /* 为 SIGINT 信号注册处理函数 */
    signal(SIGINT, sigint_handler);
    for ( ; ; )
        sleep(1);
}

程序B:

#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 (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);
    }

    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
        perror("进程 2: 文件加锁失败");
    else
        printf("进程 2: 文件加锁成功!\n");
    
    /* 写文件 */
    len = strlen(buf);
    if (0 > write(fd, buf, len)) {
        perror("write error");
        exit(-1);
    }
    printf("进程 2: 写入到文件的字符串<%s>\n", buf);
    
    /* 将文件读写位置移动到文件头 */
    if (0 > lseek(fd, 0x0, SEEK_SET)) {
        perror("lseek error");
        exit(-1);
    }

    /* 读文件 */
    memset(buf, 0x0, sizeof(buf)); //清理 buf
    if (0 > read(fd, buf, len)) {
        perror("read error");
        exit(-1);
    }
    printf("进程 2: 从文件读取的字符串<%s>\n", buf);
    
    /* 解锁、退出 */
    flock(fd, LOCK_UN);
    close(fd);
    exit(0);
}

先在后台运行程序A,对测试文件dtsfile上锁:

./flockA dstfile &

                                                

然后程序B也对测试文件上锁:

./flockB dstfile

        ​​​​​​​        

发现程序B上锁失败了,但是还是可以读取写入的,这是因为适用的是建议性锁。

然后我们给程序A发送一个信号解锁文件,再执行程序B去上锁看看可不可以:

kill -2 58314
        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        
./flockB dstfile 

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

可以发现程序B上锁成功了。

3.2 fcntl( )
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

        fcntl是一个多功能文件描述符管理工具箱,通过配合不同的 cmd 操作命令来实现不同的功能。 与锁相关的 cmd 为 F_SETLK、F_SETLKW、F_GETLK,第三个参数 flockptr 是一个 struct flock 结构体 指针。使用 fcntl()实现文件锁功能与 flock()有两个比较大的区别:

1)flock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁 /解锁,可以精确到某一个字节数据。

2) flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。

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 时有效。

注意:
1)锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
2)若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文
件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文
件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
3)如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置,并且指定
参数 l_len 等于 0。

读锁写锁规则:

  三个有关的cmd: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 命令对文件加锁之前也有可能被其它进程锁住。
 3.2.1 规则

1) 文件关闭的时候,会自动解锁。

2) 一个进程不可以对另一个进程持有的文件锁进行解锁。

3) 由 fork()创建的子进程不会继承父进程所创建的锁。

4) 当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过 复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以。

lock.l_type = F_RDLCK;
fcntl(fd, F_SETLK, &lock);//加锁
new_fd = dup(fd);
lock.l_type = F_UNLCK;
fcntl(new_fd, F_SETLK, &lock);//解锁
3.2.2 实操   
 整个文件加锁/解锁

        使用fcnt()对一个文件进行加锁和解锁测试,这里给加一个写锁,然后写入hello world。

#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;
    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);
}

新建一个测试空白文件,执行程序,传入测试文件名称,执行后会在其内看到hello world。

文件不同区域加锁/解锁

        一个进程可以对同一个文件的不同区域进行加锁,当然这两个区域不能有重叠的情况。试试用一个进程对同一文件的两个不同区域分别加读锁和写锁,对文件的 100~200 字节区间加了一个写锁, 对文件的 400~500 字节区间加了一个读锁。

#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);
}
验证读锁的共享性

        读锁是共享的,我们通过在对读锁上锁后进行死循环,然后多创建几个进程进行测试,看看是不是还能进行上锁。

#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);
}

从结果看是可以共享的,执行了很多次,发现都可以上锁。

验证写锁的独占性

        通用多次执行程序,验证能否成功。

#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);
}

通过结果发现写锁确实是独占的,只要不解锁就不能被其他进程上锁。

        

七、总结

        非阻塞 I/O:进程向文件发起 I/O 操作,使其不会被阻塞。

         I/O 多路复用:select()和 poll()函数。  

         异步 I/O:当文件描述符上可以执行 I/O 操作时,内核会向进程发送信号通知它。

         存储映射 I/O:mmap()函数。  

         文件锁:flock()、fcntl()函数的使用。

  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值