Linux-笔记 高级I/O操作

前言

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机制。

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

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)指定 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 操作。

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

设置异步 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实现读鼠标应用程序:

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

展望与优化

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

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_code | si_band 掩码值 | 描述/说明 |
| POLL_IN | POLLIN | POLLRDNORM | 可读取数据 |
| POLL_OUT | POLLOUT | POLLWRNORM | POLLWRBAND | 可写入数据 |
| POLL_MSG | POLLOUT | POLLWRNORM | POLLWRBAND | 不使用 |
| POLL_ERR | POLLERR | I/O 错误 |
| POLL_PRI | POLLPRI | POLLRDNORM | 可读取高优先级数据 |
| POLL_HUP | POLLHUP | 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操作的效率,尤其是在处理大量数据时。

实现

映射函数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 信号,该信号的系统默认操作是终止进程、并生成 核心可用于调试的核心转储文件。

解除映射函数munmap

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

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

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

实操
相关推荐
青い月の魔女20 分钟前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
赵大仁28 分钟前
在 CentOS 7 上安装 Node.js 20 并升级 GCC、make 和 glibc
linux·运维·服务器·ide·ubuntu·centos·计算机基础
qq_5895681028 分钟前
node.js web框架koa的使用
笔记·信息可视化·echarts
vvw&33 分钟前
Docker Build 命令详解:在 Ubuntu 上构建 Docker 镜像教程
linux·运维·服务器·ubuntu·docker·容器·开源
冷曦_sole1 小时前
linux-21 目录管理(一)mkdir命令,创建空目录
linux·运维·服务器
最后一个bug1 小时前
STM32MP1linux根文件系统目录作用
linux·c语言·arm开发·单片机·嵌入式硬件
dessler1 小时前
Docker-Dockerfile讲解(二)
linux·运维·docker
卫生纸不够用1 小时前
子Shell及Shell嵌套模式
linux·bash
stm 学习ing1 小时前
HDLBits训练6
经验分享·笔记·fpga开发·fpga·eda·verilog hdl·vhdl
world=hello2 小时前
关于科研中使用linux服务器的集锦
linux·服务器