高级I/O

非阻塞IO

非阻塞 IO 与阻塞IO读取数据区别

sudo od -x /dev/input/eventX
Tips:需要添加 sudo,在 Ubuntu系统下,普通用户是无法对设备文件进行读取或写入操作。 eventX对应不同的输入设备,有键盘,鼠标等。

鼠标:
在这里插入图片描述
阻塞式 I/O 读取鼠标数据:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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");
        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()会成功读取到数据并返回

在这里插入图片描述

非阻塞打开:

fd = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);

在这里插入图片描述

提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有 发生输入事件) , 是没有数据可读,故而导致失败返回,这就是非阻塞 I/O。

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

非阻塞并发读取鼠标键盘数据

使用非阻塞式 I/O 解决了阻塞式 I/O 情况下并发读取文件所出现的问题
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define MOUSE "/dev/input/event2"

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

在这里插入图片描述

IO多路复用

 I/O 多路复用方法,解决非阻塞轮询CPU 占用率特别高问题

何为IO多路复用

I/O 多路复用(IO multiplexing) 它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件) 可以执行 I/O 操作时, 能够通知应用程序进行相应的读写操作。

I/O 多路复用技术是为了解决:
在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。

由此可知, I/O 多路复用一般用于并发式的非阻塞 I/O,也就是多路非阻塞 I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。

我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用 select()和 poll()。这两个函数基本是一样的,细节特征上存在些许差别!

I/O 多路复用存在一个非常明显的特征:

  • 外部阻塞式,
  • -内部监视多路 I/O

fcntl

int fcntl(int fd, int cmd, ... /* arg */ )

函数参数和返回值含义如下:
fd: 文件描述符。
cmd: 操作命令。 此参数表示我们将要对 fd 进行什么操作, cmd 参数支持很多操作命令,大家可以打开 man 手册查看到这些操作命令的详细介绍,这些命令都是以 F_XXX 开头的,譬如 F_DUPFD、 F_GETFD、F_SETFD 等, 不同的 cmd 具有不同的作用, cmd 操作命令大致可以分为以下 5 种功能:

  • 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
  • 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD);
  • 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
  • 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN);
  • 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);

select


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

    *@param[nfds]		:nfds 通常表示最大文件描述符编号值加 1,考虑 readfds、writefds 以及 exceptfds这三个文件描述符集合,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是参数 nfds。

    *@param[readfds]    :检测读是否就绪(是否可读)的文件描述符集合
    *@param[writefds]   :检测写是否就绪(是否可写)的文件描述符集合;
    *@param[exceptfds]  :检测异常情况是否发生的文件描述符集合。
    *@param[timeout]    :对 readfds、 writefds 以及 exceptfds 中的某些事件不感兴趣,可将其设置为 NULL,这表示对相应条件不关心。
                        如果这三个参数都设置为 NULL,则可以将 select()当做为一个类似于 sleep()休眠的函数来使用,通过 select()函数的最后一个参数 timeout 来设置休眠时间。

                        timeout 参数设置为 NULL,表示 select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;

    *@return 			:返回-1 表示有错误发生,并且会设置 errno。
                        返回 0 表示在任何文件描述符成为就绪态之前 select()调用已经超时, 在这种情况下, readfds,writefds 以及 exceptfds 所指向的文件描述符集合都会被清空。
                        返回一个正整数表示有一个或多个文件描述符已达到就绪态。

    *@others			:fd_set 类型指针,指向一个 fd_set 类型对象, fd_set 数据类型是一个文件描述符的集合体,所以参数 readfds、 writefds 以及exceptfds 都是指向文件描述符集合的指针.
*/

/*
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    //文件描述符操作宏
    void FD_CLR(int fd, fd_set *set);
    int FD_ISSET(int fd, fd_set *set);
    void FD_SET(int fd, fd_set *set);
    void FD_ZERO(fd_set *set);

    FD_ZERO()将参数 set 所指向的集合初始化为空;
    FD_SET()将文件描述符 fd 添加到参数 set 所指向的集合中;
    FD_CLR()将文件描述符 fd 从参数 set 所指向的集合中移除;
    如果文件描述符 fd 是参数 set 所指向的集合中的成员,则 FD_ISSET()返回 true,否则返回 false。
    文件描述符集合有一个最大容量限制,有常量 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
*/

#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/event2"

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 (0 > ret)
        {
            perror("select error");
            goto out;
        }

        else if (0 == ret)
        {
            fprintf(stderr, "select timeout.\n");
            continue;
        }

        /* 检查键盘是否为就绪态 */
        if (FD_ISSET(0, &rdfds))
        {
            ret = read(0, 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);
}

在这里插入图片描述

一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)也就是FD_ISSET函数返回1,也就是鼠标键盘有动作。

poll

struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指 定需要为文件描述符 fd 做检查的事件。当 poll()函数返回时, revents 变量由 poll()函数内部进行设置,用于 说明文件描述符 fd 发生了哪些事件(注意, poll()没有更改 events 变量) ,我们可以对 revents 进行检查,判 断文件描述符 fd发生了什么事件

在这里插入图片描述

在实际应用编程中,一般用的最多的还是 POLLIN 和 POLLOUT
/*
    *@brief				:与select()函数很相似

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

    *@return 			:返回-1 表示有错误发生,并且会设置 errno。
                        -返回 0 表示该调用在任意一个文件描述符成为就绪态之前就超时了。
                        -返回一个正整数表示有一个或多个文件描述符处于就绪态了, 返回值表示 fds 数组中返回的 revents变量不为 0 的 struct pollfd 对象的数量。
*/

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

#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/event2"

int main(void)
{
    char buf[100];
    int fd, ret = 0, flag;
    fd_set rdfds;
    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("select error");
            goto out;
        }

        else if (0 == ret)
        {
            fprintf(stderr, "select 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);
}

在这里插入图片描述

异步IO

//以异步 I / O 方式读取鼠标,当进程接收到 SIGIO 信号时,执行信号处理函数 sigio_handler() ,在该函数中调用 read() 读取鼠标数据。 示例代码 13.3.1 以异步 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>

#define MOUSE "/dev/input/event2"
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);
}

在这里插入图片描述

优化异步IO

异步io与select poll区别及应用场景:

  • 在一个需要同时检查大量文件描述符(譬如数千个)的
    应用程序中,例如某种类型的网络服务端程序,与 select()和 poll()相比,异步 I/O 能够提供显著的性能优势。
  • 之所以如此,原因在于:对于异步 I/O,内核可以“记住”要检查的文件描述符,且仅当这些文件描述符上可执行 I/O 操作时,内核才会向应用程序发送信号。
  • 而对于 select()或 poll()函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O 操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检查操作。
  • 当需要检查的文件描述符并不是很多时,使用 select()或 poll()是一种非常不错的方案!

优化:

  • 默认的异步 I/O 通知信号 SIGIO 是非排队信号。 SIGIO 信号是标准信号(非实时信号、不可靠信号),所以它不支持信号排队机制, 譬如当前正在执行 SIGIO 信号的处理函数,此时内核又发送多次 SIGIO 信号给进程,这些信号将会被阻塞,只有当信号处理函数执行完毕之后才会传递给进程,并且只能传递一次,而其它后续的信号都会丢失。
  • 无法得知文件描述符发生了什么事件。 在上述代码中的信号处理函数 sigio_handler()中, 直接调用了 read()函数读取鼠标,而并未判断文件描述符是否处于可读就绪态,事实上, 上述代码这种异步 I/O 方式并未告知应用程序文件描述符上发生了什么事件,是可读取还是可写入亦或者发生异常等。

使用实时信号替换默认信号 SIGIO

fcntl(fd, F_SETSIG, SIGRTMIN);

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

在这里插入图片描述
在这里插入图片描述

_GNU_SOURCE 宏可用于开启/禁用 Linux 系统调用和 glibc 库函数的一些功能、特性,要打开这些特性,需要在应用程序中定义该宏,定义该宏之后意味着用户应用程序打开了所有的特性;默认情况下,_GNU_SOURCE 宏并没有被定义,所以当使用到它控制的一些特性时,应用程序编译将会报错!定义该宏的方式有两种:

  • 直接在源文件中定义: #define _GNU_SOURCE
  • gcc 编译时使用-D 选项定义_GNU_SOURCE 宏:
gcc -D_GNU_SOURCE -o testApp testApp.c
#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>

#define MOUSE "/dev/input/event2"
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);
}

在这里插入图片描述

存储映射IO

文件描述词取得文件状态fstat

fstat 与 stat 区别在于

  • stat 是从文件名出发得到文件属性信息,不需要先打开文件;
  • 而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符。
  • 具体该用 stat还是 fstat,看具体的情况;
  • 譬如,并不想通过打开文件来得到文件属性信息,那么就使用 stat,如果文件已经打开了,那么就使用 fstat。

截断文件ftruncate

使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度,

这两个函数的区别在于:

  • ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路径 path 来指定目标文件,其功能一样。

13.5.1 mmap、munmap、mprotect

在这里插入图片描述

/*
    *@brief :调用该函数,内核将一个给定的文件映射到进程地址空间中的一块内存区域中

    *@param[addr]   :指定映射到内存区域的起始地址。通常将其设置为 NULL

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

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

                    - prot 指定为为 PROT_NONE,也可将其设置为 PROT_EXEC、 PROT_READ、 PROT_WRITE 中一个或多个(通过按位或运算符任意组合)。
                     对指定映射区的保护要求不能超过文件 open()时的访问权限,
                     譬如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。

    *@param[flags]  : flags 可影响映射区的多种属性, 参数 flags 必须要指定以下两种标志之一:
                    - MAP_SHARED: 此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
                    - MAP_PRIVATE: 此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-onwrite),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。除此之外,还可将以下标志中的 0 个或多个组合到参数 flags 中,通过按位或运算符进行组合:
                    - MAP_FIXED: 在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃!通常,不建议使用此标志,因为这不利于移植。
                    - MAP_ANONYMOUS: 建立匿名映射, 此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域无法和其它进程共享。
                    - MAP_ANON: 与 MAP_ANONYMOUS 标志同义,不建议使用。
                    - MAP_DENYWRITE: 该标志被忽略。
                    - MAP_EXECUTABLE: 该标志被忽略。
                    - MAP_FILE: 兼容性标志,已被忽略。
                    - MAP_LOCKED: 对映射区域进行上锁。

                    通常情况下,参数 flags 中只指定了 MAP_SHARED。

    *@param[fd]     :文件描述符,指定要映射到内存区域中的文件。
    *@param[offset] :文件映射的偏移量,通常将其设置为 0

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

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

系统调用 mprotect()可以更改一个现有映射区的保护要求
int mprotect(void *addr, size_t len, int prot);

*/

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

int main(int argc, char *argv[])
{
    //定义目标文件和源文件描述符
    int srcfd, dstfd;

    //定义源文件起始地址
    void *srcaddr;

    //定义目标文件起始地址
    void *dstaddr;
    int ret;
    struct stat sbuf;

    //判断输入是否有2个参数
    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");
        ret = -1;
        goto out1;
    }

    /* 打开目标文件 */
    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 out3;
    }

    /* 将目标文件映射到内存区域中 */
    dstaddr = mmap(NULL, sbuf.st_size,
                   PROT_WRITE, MAP_SHARED, dstfd, 0);
    if (MAP_FAILED == dstaddr)
    {
        perror("mmap error");
        ret = -1;
        goto out4;
    }

    /* 将源文件中的内容复制到目标文件中 */
    memcpy(dstaddr, srcaddr, sbuf.st_size);
    ret = 0;
    goto out4;

    /* 程序退出前清理工作 */
out4:
    /* 解除目标文件映射 */
    munmap(dstaddr, sbuf.st_size);
    goto out3;
out3:
    /* 解除源文件映射 */
    munmap(srcaddr, sbuf.st_size);
    goto out2;
out2:
    /* 关闭目标文件 */
    close(dstfd);
    goto out1;
out1:
    /* 关闭源文件并退出 */
    close(srcfd);
    exit(ret);
}

在这里插入图片描述

msync

  • read()和 write()系统调用在操作磁盘文件时不会直接发起磁盘访问(读写磁盘硬件),而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,在后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘中,所以由此可知,调用 write()写入到磁盘文件中的数据并不会立马写入磁盘,而是会先缓存在内核缓冲区中,所以就会出现 write()操作与磁盘操作并不同步,也就是数据不同步。

  • 对于存储 I/O 来说亦是如此,写入到文件映射区中的数据也不会立马刷新至磁盘设备中,而是会在我们将数据写入到映射区之后的某个时刻将映射区中的数据写入磁盘中。

  • 所以会导致映射区中的内容与磁盘文件中的内容不同步。我们可以调用 msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作) ,系统调用 msync()类似于 fsync()函数,不过 msync()作用于映射区。

int msync(void *addr, size_t length, int flags);

参数 flags 应指定为 MS_ASYNC 和 MS_SYNC 两个标志之一,
除此之外,还可以根据需求选择是否指定 MS_INVALIDATE 标志,作为一个可选标志。

  • MS_ASYNC: 以异步方式进行同步操作。调用 msync()函数之后,并不会等待数据完全写入磁盘之后才返回。
  • MS_SYNC: 以同步方式进行同步操作。调用 msync()函数之后,需等待数据全部写入磁盘之后才返回。
  • MS_INVALIDATE: 是一个可选标志, 请求使同一文件的其它映射无效(以便可以用刚写入的新
    值更新它们) 。

munmap()函数并不影响被映射的文件,也就是说,当调用 munmap()解除映射时并不会将映射区中的内容写到磁盘文件中。
如果 mmap()指定了 MAP_SHARED 标志,对于文件的更新,会在我们将数据写入到映射区之后的某个时刻将映射区中的数据更新到磁盘文件中,由内核根据虚拟存储算法自动进行。如果 mmap()指定了 MAP_PRIVATE 标志,在解除映射之后,进程对映射区的修改将会丢弃!

普通 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 的实质其实是共享, 与 IPC 之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O 方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中,如下所示:在这里插入图片描述
  • 而对于存储映射 I/O 来说,由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映射区来实现文件复制,如下所示:

在这里插入图片描述

  • 使用存储映射 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 会在视频图像处理方面用的比较多,
  • 譬如在第二篇内容,我们将会介绍 Framebuffer 编程,通俗点说就是 LCD 编程,就会使用到存储映射 I/O。

文件锁

线程资源保护:

前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制, 锁机制实现用于对共享资源的访问进行保护;
只不过互斥锁、自旋锁、 读写锁与文件锁的应用场景不一样, 互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。

文件资源保护:

而文件锁, 顾名思义是一种应用于文件的锁机制, 当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性, linux通常采用的方法是对文件上锁, 来避免多个进程同时操作同一文件时产生竞争状态。

譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。

文件锁作用:

一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁, 来避免访问共享资源产生竞争 状态。

文件锁的分类

文件锁可以分为建议性锁和强制性锁两种:

  • 建议性锁
    建议性锁本质上是一种协议,那么大家就要遵守协议, 访问文件之前先对文件上锁。
    这就好比交通信号灯,规定红灯不能通行,绿灯才可以通行,但如果你非要在红灯的时候通行,谁也拦不住你,那么后果将会导致发生交通事故;所以必须要大家共同遵守交通规则, 交通信号灯才能起到作用。
  • 强制性锁:
    强制性锁比较好理解,它是一种强制性的要求, 如果进程对文件上了强制性锁,其它的进程在没有获取到文件锁的情况下是无法对文件进行访问的。其本质原因在于, 强制性锁会让内核检查每一个 I/O 操作。

flock()函数加锁

flock:

/*
    *@brief              :对文件加锁或者解锁,但是 flock()函数只能产生建议性锁

    *@param[fd]          :文件描述符,指定需要加锁的文件。

    *@param[operation]  :LOCK_SH: 在 fd 引用的文件上放置一把共享锁。 所谓共享,指的便是多个进程可以拥有对同一个文件的共享锁,该共享锁可被多个进程同时拥有。
                        -LOCK_EX: 在 fd 引用的文件上放置一把排它锁(或叫互斥锁) 。 所谓互斥,指的便是互斥锁只能同时被一个进程所拥有。
                        -LOCK_UN: 解除文件锁定状态,解锁、释放锁。除了以上三个标志外,还有一个标志:
                        -LOCK_NB: 表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、 直到其它进程释放锁为止, 如果不想让程序被阻塞,
                            可以指定 LOCK_NB 标志, 如果无法获取到锁应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK) ,
                            通常与 LOCK_SH 或 LOCK_EX一起使用,通过位或运算符组合在一起。

    *@return            :成功将返回 0;失败返回-1、并会设置 errno,

    *@others            :需要注意的是,同一个文件不会同时具有共享锁和互斥锁
*/

// int flock(int fd, int operation);

//使用 flock() 对文件加锁 / 解锁

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

//加锁成功之后,程序进入了 for 死循环,一直持有锁;此时我们可以执行另一个程序,

uflock:

//该程序首先也会打开文件,文件路径通过传参的方式传递进来,同样在程序中也会调用 flock()函数对
//文件加锁(排它锁、非阻塞方式) ,不管加锁成功与否都会执行下面的 I/O 操作,将数据写入文件、在读取出来并打印。

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

在这里插入图片描述
uflock加锁失败,原因在于锁已经被 flock 进程所持有,所以加锁自然会失败;但是可以发现虽然加锁失败,但是 testApp2 对文件的读写操作是没有问题的是
成功的。
这就是建议性锁的特点;正确的使用方式是,在加锁失败之后不要再对文件进行 I/O 操作了,遵循这个协议。

发送信号SIGIO 解锁,再读取:
在这里插入图片描述

符号进程运行状态
R正在运行,或在队列中的进程
S处于休眠状态
T停止或被追踪
Z僵尸进程
W进入内存交换(从内核2.6开始无效)
X死掉的进程
<高优先级
N低优先级
L有些页被锁进内存
s包含子进程
+位于后台的进程组;
l多线程,克隆线程

在这里插入图片描述
这里也可以用Ctrl+C发送信号,Ctrl+Z退出
在这里插入图片描述
关于 flock()的几条规则:

  • 同一进程对文件多次加锁不会导致死锁。 当进程调用 flock()对文件加锁成功,再次调用 flock()对文件(同一文件描述符) 加锁,这样不会导致死锁,新加的锁会替换旧的锁。
    譬如调用 flock()对文件加共享锁,再次调用 flock()对文件加排它锁,最终文件锁会由共享锁替换为排它锁。
  • 文件关闭的时候,会自动解锁。 进程调用 flock()对文件加锁,如果在未解锁之前将文件关闭,则会导致文件锁自动解锁,也就是说,文件锁会在相应的文件描述符被关闭之后自动释放。
    同理,当一个进程终止时,它所建立的锁将全部释放。
  • 一个进程不可以对另一个进程持有的文件锁进行解锁。
  • 由 fork()创建的子进程不会继承父进程所创建的锁。
    这意味着,若一个进程对文件加锁成功,然后该进程调用 fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子进程从父进程继承了其文件描述符,但不能继承文件锁。
    这个约束是有道理的,因为锁的作用就是阻止多个进程同时写同一个文件,如果子进程通过 fork()继承了父进程的锁,则父进程和子进程就可以同时写同一个文件了

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

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

这段代码先在 fd 上设置一个排它锁,然后使用 dup()对 fd 进行复制得到新文件描述符 new_fd,最后通过 new_fd 来解锁,这样可以解锁成功。
但是, 如果不显示的调用一个解锁操作,只有当所有文件描述符都被关闭之后锁才会被释放。
譬如上面的例子中,如果不调用 flock(new_fd, LOCK_UN)进行解锁,只有当 fd和 new_fd 都被关闭之后锁才会自动释放。

fcntl

lseek

off_t lseek(int fd, off_t offset, int whence);
  • offset: 偏移量,以字节为单位。
  • whence: 用于定义参数 offset 偏移量对应的参考值, 该参数为下列其中一种(宏定义) :
    SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算) ;
    SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
    SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移

使用示例:

(1)将读写位置移动到文件开头处:
off_t off = lseek(fd, 0, SEEK_SET);
if (-1 == off)
return -1;
(2)将读写位置移动到文件末尾:
off_t off = lseek(fd, 0, SEEK_END);
if (-1 == off)
return -1;
(3)将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)
return -1;
(4)获取当前读写位置偏移量:
off_t off = lseek(fd, 0, SEEK_CUR);
if (-1 == off)
return -1;
函数执行成功将返回文件当前读写位置。
/*
    *@brief              :对文件加锁或者解锁,但是 flock()函数只能产生建议性锁

    *@param[fd]          :文件描述符,指定需要加锁的文件。

    *@param[cmd]         :F_SETLK 这种用法一般用于测试,测试调用进程对文件加一把由参数 flockptr 指向的 struct flock对象所描述的锁是否会加锁成功
                          F_SETLKW 对文件添加由 flockptr 指向的 struct flock 对象所描述的锁
                          F_GETLK 此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进程将会进入阻塞状态。 只有当请求的锁可用时,进程才会被唤醒。

                          F_GETLK 命令一般很少用, 事先用 F_GETLK 命令测试是否能够对文件加锁,然后再用 F_SETLK 或
                          F_SETLKW 命令对文件加锁,但这两者并不是原子操作, 所以即使测试结果表明可以加锁成功,但是在使
                          用 F_SETLK 或 F_SETLKW 命令对文件加锁之前也有可能被其它进程锁住。

    *@param[operation]  :对 struct flock 结构体说明如下:
                        - l_type: 所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一,
                            F_RDLCK表示共享性质的读锁, F_WRLCK 表示独占性质的写锁, F_UNLCK 表示解锁一个区域。

                        - l_whence 和 l_start: 这两个变量用于指定要加锁或解锁区域的起始字节偏移量,与 的 lseek()函数中的 offset 和 whence 参数相同,

                        - l_len: 需要加锁或解锁区域的字节长度。

                        - l_pid: 一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。




    *@return            :成功将返回从文件头部开始算起的位置偏移量(字节为单位), 也就是当前的读写位置; 发生错误将返回-1。

    *@others            :与flock区别:
                        -flock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁解锁,可以精确到某一个字节数据。
                        -flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。

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

// int fcntl(int fd, int cmd, ... struct flock *flockptr );

#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);
    printf("对文件解锁成功!\n");

    /* 退出 */
    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 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);
    printf("解锁成功!\n");
    /* 退出 */
    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);
}

在这里插入图片描述
改为写加锁就如下:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值