• Linux 系统编程学习笔记


    概述

    主要高级IO:

    1. 非阻塞IO
    2. 记录锁(文件锁)
    3. IO多路复用(I/O multiplexing)
    4. 异步IO
    5. 存储映射

    高级IO,涉及到文件的IO操作,必然会用到文件描述符(fd),而且依赖于fcntl函数支持。

    非阻塞IO

    阻塞读文件

    当读某些文件时,如果文件没有数据,会导致读操作阻塞,如:

    1. 读鼠标/键盘等字符设备文件;
    2. 读管道文件(PIPE,FIFO);

    示例,读取键盘输入(字符设备文件)阻塞:
    下面如果没有用户输入,进程会阻塞在read函数处

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main() {
      char s[100];
      int n = 0;
      // int coordinate; // 鼠标光标坐标
      // int mousefd = open("/dev/input/mouse0", O_RDONLY); // 打开鼠标光标文件,用于读取鼠标数据
      while (1) {
        printf("plz input:
    ");
        n = read(stdin, s, sizeof s);
        // n = read(mousefd, &coordinate, sizeof coordinate);
        if (n > 0) printf("%s
    ", s);
        printf("You have input data
    ");
      }
    }
    

    问题:
    1. 读普通文件会阻塞吗?
    读普通文件不会阻塞,因为有数据就成功返回,没有数据就返回0,并不会阻塞;

    2. 写文件会阻塞吗?
    写文件会阻塞,如写管道文件,会需要先从读端把管道文件读取出来。但是写普通文件是不会阻塞的。

    阻塞读文件的意义

    文件没有数据读取而阻塞,导致线程进入阻塞状态并不会占用CPU,节省CPU资源。当然特殊情况下,如果需要非阻塞读,OS也提供了非阻塞读文件的方式。

    如何以非阻塞方式读文件

    两种方式,本质都是添加O_NONBLOCK选项到文件状态标志:
    1. open文件时指定O_NONBLOCK选项

    还没有open的文件,在读取鼠标光标坐标的例子上,open时添加O_NONBLOCK参数

    int mousefd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK); // 非阻塞方式读取鼠标文件
    int ret;
    int coordinate;
    
    while (1) {
      ret = read(mousefd, &coordinate, sizeof coordinate); // 没读取到数据时,报EAGAIN错误(errno = EAGAIN)
      ...
    }
    

    2. fcntl修改打开文件属性,指定O_NONBLOCK选项

    已经open的文件,如果没有指定O_NONBLOCK选项,可用通过fctnl修改打开文件的属性,增加该选项。

    例,将0(stdin 标准输入设备)用fcntl设为O_NONBLOCK

    #include <stdio.h>
    
    // 重新设置
    // 根据F_SETFL命令选项重设stdin对应文件的已打开
    fcntl = (stdin, F_SETFL, O_RDONLY | O_NONBLOCK); 文件状态标志
    
    // 补充设置
    flg = fcntl(stdin, F_GETFL); // 获取原有的已打开文件状态标志
    flg |= O_NONBLOCK; // 追加O_NONBLOCK文件状态标志
    fcntl(stdin, F_SETFL, flg); // 将修改后的文件状态标志写回
    

    问题:
    1. stdion文件状态为O_NONBLOCK,scanf会阻塞吗?
    scanf不会阻塞,因为scanf底层也是调用read(stdin, ...)来实现的。既然read不会阻塞,scanf也不会。

    记录锁(文件锁)

    进程有进程信号量加锁机制,线程有互斥量、条件变量、信号量的加锁机制,而文件也有加锁机制,称为文件锁。文件锁也称为记录锁,主要分为建议锁和强制性锁。

    加锁对象 描述
    进程 进程有进程信号量加锁机制
    线程 线程有互斥量,条件变量,信号量加锁机制
    文件 文件锁机制,主要分为建议锁和强制性锁。fcntl是建议锁,也是最常用的;强制锁只用于协作进程

    文件锁的作用

    文件锁用来保护文件数据。多个进程/线程读写同一个文件时,为避免进程各自同时读写文件造成干扰,产生“脏数据”,可用使用进程信号量互斥实现,除了可用使用进程信号量外,还可用使用文件锁。

    多个进程同时读写同一个文件的情形:
    1. 写写互斥
    某个进程正在写文件时,其他进程不能写,否则会破坏写数据的完整性。

    2. 读写互斥
    1)某个进程正在写文件,其他进程不能读,否则读出的数据不完整;
    2)某个进程正在读数据,其他进程不能写,否则读出的数据不完整;

    3. 读读共享
    某个进程读数时,其他进程也读取,但是不会破坏数据完整性,无需担心数据相互干扰问题。

    信号量的局限性
    多个进程同时读写文件的情形,使用信号量只会每种情形都互斥,难以实现读读共享。因为信号量并不直接识别线程操作是读操作,还是写操作。而使用文件锁,可用既做到互斥,又能做到共享。

    使用文件锁加锁

    读锁 & 写锁

    对文件加锁可用分为两种锁:读文件锁(简称读锁),写文件锁(简称写锁)。
    读锁、写锁之间关系:

    1. 读锁和读锁共享:可用重复加读锁,别人加了读锁在没有解锁前,我们依然可用继续加读锁;
    2. 读锁,写锁互斥:别人加读锁没有解锁前,我们加写锁会失败;别人加写锁,我们加读锁会失败。
    3. 写锁,写锁互斥:别人加了写锁没解锁前,我们不能加写锁,加写锁会失败;

    加锁失败后2种处理方式:

    • 阻塞加锁 - 阻塞,直到别人解锁,然后我们加锁成功;(常用)
    • 非阻塞加锁 - 出错返回,不阻塞;

    文件锁的加锁方式

    1. 对整个文件内容加锁
    最常用方式是对整改文件加锁。如果文件长度变化,加锁内容的长度也会自带变化。

    2. 对文件部分内容加锁
    对文件加区域锁,即对文件指定区域范围内容加锁。一般地,对多少内容加锁,就对多少内容解锁。

    文件锁的实现

    需要用到fcntl。

    fcntl

    fctnl函数原型

    #include <unistd.h>
    #include <fcntl.h>
    
    // 第三个参数...,变参数,用到时才写
    int fcntl(int fd, int cmd, .../* struct flock *flock */);
    
    1. 功能
      cmd设置为与文件锁相关宏时(见下参数描述),fcntl用于实现文件锁。

    2. 返回值
      成功返回0,失败-1,errno被设置。

    3. 参数
      1)fd 文件描述符,指向要被加锁的文件;
      2)cmd 实现文件锁时,有三种可选设置:F_GETLK、F_SETLK、F_SETLKW,都需要用到第三个参数struct flock *flockptr。

    • F_GETLK 从内核获取文件锁的信息,将其保存到第三个参数;
    • F_SETLK 设置非阻塞文件锁,第三个参数传入锁设置;
    • F_SETLKW 设置阻塞文件锁,第三个参数传入锁设置;

    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 offet for lock
      off_t l_len; // Number of bytes to lock
      pid_t l_pid; // PID of process blocking our lock(F_GETLK only)
    }
    

    结构体成员:

    • l_type 锁类型
    • F_RDLCK 读锁,共享锁
    • F_WRLCK 写锁
    • F_UNLCK 解锁
    • l_whence 加锁位置粗定位
    • SEEK_SET 文件开头
    • SEEK_CUR 文件当前位置
    • SEEK_END 文件末尾位置

    l_whence含义同lseek函数的whence off_t lseek(int fd, off_t offset, int whence);

    • l_start 加锁位置精确定位,相对于l_whence的偏移,与lseek offset含义相同。l_whence + l_start 确定加锁起始位置。通常,l_whence = SEEK_SET, l_start = 0表从文件头开始加锁。
    • l_lend 从l_whence, l_start所指定的起始点算起,需要对多长内容加锁。如果l_len = 0,表示从起始地点一直加锁到末尾。即使文件长度变化,也将自带加锁到末尾。
    • l_pid 当前正对文件加锁的进程PID,由文件锁自动设置。

    加锁位置小结

    1)位置
    起始地址 begin = l_whence + l_start
    长度 len = l_len

    2)对整个文件加锁
    l_whence = SEEK_SET, l_start = 0, l_len = 0

    例:文件锁的使用

    完整源码:advancedio模块 | gitee
    对文件锁filelock模块进行封装,主要以对整个文件加写锁、解写锁、加读锁、解读锁 4种方式:

    // file.h
    void filelock_lockwrite(int fd, int block);
    void filelock_unlockwrite(int fd);
    void filelock_lockread(int fd, int block);
    void filelock_unlockread(int fd);
    
    // filelock.c
    
    /**
     * 通过fcntl修改文件加锁/解锁方式
     */
    static void filelock_set(int fd, int iswait, int l_type, int l_whence, int l_start, int l_len) {
        // 设置文件锁相关flock属性 为整个文件加锁
        struct flock flock1;
        flock1.l_type = l_type; // 写锁
        flock1.l_whence = l_whence; // 起点在文件头
        flock1.l_start = l_start;
        flock1.l_len = l_len;
    
        int ret = fcntl(fd, iswait, flock1);
        if (ret == -1) {
            perror("fcntl fail");
            exit(-1);
        }
    }
    
    /**
     * 文件写加锁
     * @param fd 已打开文件描述符
     * @param block 是否阻塞方式对文件加锁
     */
    void filelock_lockwrite(int fd, int block) {
        int iswait = block ? F_SETLKW : F_SETLK;
        filelock_set(fd, iswait, F_WRLCK, SEEK_SET, 0, 0);
    }
    
    /**
     * 文件写解锁
     * @param fd 已打开文件描述符
     */
    void filelock_unlockwrite(int fd) {
        filelock_set(fd, F_UNLCK, F_WRLCK, SEEK_SET, 0, 0);
    }
    
    /**
     * 文件读加锁
     * @param fd 已打开文件描述符
     * @param block 是否阻塞方式对文件加锁
     */
    void filelock_lockread(int fd, int block) {
        int iswait = block ? F_SETLKW : F_SETLK;
        filelock_set(fd, iswait, F_RDLCK, SEEK_SET, 0, 0);
    }
    
    /**
     * 文件读解锁
     * @param fd 已打开文件描述符
     */
    void filelock_unlockread(int fd) {
        filelock_set(fd, F_UNLCK, F_RDLCK, SEEK_SET, 0, 0);
    }
    

    客户端:2个线程,每隔1秒同时对同一个文件进行写操作

    #define FILE_PATH  "./text.txt"
    
    static void print_err(char *str, int line, int err_no) {
        printf("line %d, %s: %s
    ", line, str, strerror(err_no));
        exit(-1);
    }
    
    void *th_fun(void *arg) {
        /* 如果不加O_APPEND追加标志,可能出现内容覆盖情况:
         * 因为线程A open以后,写位置在固定位置,线程B open甚至写了某些内容后,线程A的写位置正常是要移动到末尾,
         * 而没有O_APPEND标志时,线程A并不会移动写位置,这样容易出现相互覆盖的情况
         * */
        int fd = open(FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0664);
        if (fd < 0) print_err("open file fail", __LINE__, errno);
    
        int whichth = *(int *)arg;
        char *str1;
        char *str2;
    
        if (whichth == 0) {
            str1 = "A:hello ";
            str2 = "world";
        }
        else {
            str1 = "B:1111 ";
            str2 = "22222222";
        }
    
        int len1 = strlen(str1);
        int len2 = strlen(str2);
    
        while (1) {
    //        flock(fd, LOCK_EX);
            filelock_lockwrite(fd, 1);
    
            write(fd, str1, len1);
            write(fd, str2, len2);
            write(fd, "
    ", 1);
    
            filelock_unlockwrite(fd);
    //        flock(fd, LOCK_UN);
        }
    }
    
    /**
     * 示例:2个线程同时写同一个文件,对文件进行写加锁,确保文件内容完整性
     */
    void testfilelock() {
        pthread_t th1, th2;
        void *tret;
        int thno1 = 0, thno2 = 1;
        pthread_create(&th1, NULL, th_fun, &thno1);
        pthread_create(&th2, NULL, th_fun, &thno2);
    
        pthread_join(th1, &tret);
        pthread_join(th2, &tret);
    }
    
    int main() {
        testfilelock();
        return 0;
    }
    

    运行结果(约若干秒后终止程序,查看test.txt文件):
    线程1/2 分别竞争向同一文件写入不同内容,线程1下"A:hello world",线程2写"B:111 22222222"。可用从下面的结果看到,并没有出现写的内容相互串扰的情况。

    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    A:hello world
    B:1111 22222222
    ...
    

    flock函数

    函数原型

    #include <sys/file.h>
    
    int flock(int fd, int operation);
    
    1. 功能
      按operation要求,对fd所指文件加对应文件锁。

    2. 返回值
      成功返回0;失败-1,设置errno。

    3. 参数
      1)fd 文件描述符,指向要加锁的文件
      2)operation :

    • LOCK_SH 加共享锁
    • LOCK_EX 加互斥锁
    • LOCK_UN 解锁

    flock应用于多进程

    flock用于多进程时,各个进程必须独立open文件(子进程不能继承父进程open得到的fd),而且open时须指定O_APPEND选项;否则(不指定O_APPEND),会出现相互覆盖的情况。
    注意:fcntl实现的文件锁,子进程可用使用父进程open得到的fd(反过来也可用),进行加锁/解锁。

    锁之间的关系:

    • 共享锁与互斥锁互斥;
    • 互斥锁与互斥锁互斥;
    • 共享锁与共享锁共享;

    flock应用于多线程

    flock用于多线程时,同应用于多进程,各线程必须独立open文件,而且open时必须指定O_APPEND选项。

    // 只修改上面例子中线程函数循环部分
        while (1) {
            flock(fd, LOCK_EX); // 互斥锁
    //        filelock_lockwrite(fd, 1);
    
            write(fd, str1, len1);
            write(fd, str2, len2);
            write(fd, "
    ", 1);
    
    //        filelock_unlockwrite(fd);
            flock(fd, LOCK_UN); // 解锁
        }
    

    IO多路复用(I/O multiplexing)

    解决同时“读鼠标”和“读键盘”的问题(2个阻塞字符输入设备)的方法:

    1. 多进程;
    2. 多线程;
    3. 将“读鼠标”和“读键盘”设置为非阻塞实现;
    4. 多路IO;

    多路IO工作原理

    多路IO工作原理如下图所示,

    注意:

    1. 只有读操作阻塞的fd,用多路IO才有意义;
    2. 休眠时,监听机制依然有效,能监听到有数据到来;

    多路IO的优势

    针对类似于同时读取鼠标/键盘数据的情况而言,

    1. 多进程
      开销太大,不建议使用。

    2. 非阻塞方式
      需要搭配while循环不断轮询,耗费CPU资源,不建议使用。

    3. 多线程
      开销较低,常用方法

    4. 多路IO
      使用多路IO时,由于监听时如果没有动静(没有数据到来),监听线程休眠,开销也很低。

    select和poll

    多路IO两种实现方式:select, poll。select更常用。

    select

    原型

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

    select, pselect差别在于超时参数timeout类型不一样, select的timeout是struct timeval类型的, 表示精确到1us; pselect的timeout是struct timespec类型的, 精确到1ns, 精确到1ns. 当然, 实际时间精度取决于系统的时钟精度.

    参数
    nfds 文件描述符的最大编号 + 1, 不会超过RLIMIT_NOFILE (见getrlimit获得), 实际编程按最大文件描述符编号 + 1
    readfds 可读文件描述符集合
    writefds 可写文件描述符集合
    exceptfds 异常文件描述符集合
    timeout 超时时间.

    • NULL 永远等待, 如果捕捉到信号就中断, 并返回-1, 置errno = EINTR
    • timeout->tv_sec为0, timeout->tv_usec为0, 不等待, 立即返回
    • timeout->tv_sec > 0 或timeout->tv_usec > 0, 等待指定时间(秒, 微秒). 超时后返回0

    返回值
    正常返回处于可读、可写、异常条件的描述符集合的所有数量; 超时, 返回0; 被中断, 返回-1

    示例:
    select示例, 文件名: select.c

    int ret;
    char buf[100];
    int  mousepos;
    
    // /dev/input/mouse? 根据实际情况, 测试取值
    int mousefd = open("/dev/input/mouse0", O_RDONLY);
    
    fd_set readfds;
    struct timeval timeover;
    
    while (1) {
        // select可能修改最后一个参数(超时时间)的值, 所以不能期望其值不变
        // 这是因为select可能中断返回, 超时时间会被修改为剩余时间
        timeover.tv_sec = 2;
        timeover.tv_usec = 0;
    
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);
        FD_SET(mousefd, &readfds);
    
        do {
            ret = select(mousefd + 1, &readfds, NULL, NULL, &timeover);
        } while (ret < 0 && errno == EINTR);
    
        if (ret < 0 && errno != EINTR) {
                perror("select error");
                exit(1);
        }
        else if (ret > 0) {
            if (FD_ISSET(STDIN_FILENO, &readfds)) { // 标准输入设备对应fd置位, 也就是有数据
                memset(buf, 0, sizeof(buf));
                int readret = read(STDIN_FILENO, buf, sizeof(buf));
                if (readret > 0) printf("%s
    ", buf);
            }
    
            if (FD_ISSET(mousefd, &readfds)) {
                mousepos = 0;
                int readret = read(mousefd, &mousepos, sizeof(mousepos));
                if (readret > 0) printf("%d
    ", mousepos);
            }
        }
        else if (ret == 0) { // select 超时
            printf("time out
    ");
        }
    
    }
    
    close(mousefd);
    

    poll

    原理类似select, 不过接口不一样, select接收3个集合(写集合,读集合,异常集合),而poll接收一个数组。

    原型

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

    功能
    监听集合有没有动静.如果没有动静,就阻塞;如果有,就成功返回,返回值为集合中有动静fd数量.

    参数

    1. fds 要监听的fd数组,写成struct pollfd fds[]更好理解.
      struct pollfd定义及设置:
    struct pollfd {
      int fd; // 文件描述符
      short events;  // 设置希望发生的事件, 如读事件,自定义
      short revents; // 实际发生的事件,如读事件,由poll机制自行设置
    }
    
    // pollfd数组设置
    struct pollfd fds[2];
    fds[0].fd = 0; // 监听fd = 0的文件
    fds[0].events = POLLIN; // 希望监听发生读事件(输入事件)
    
    fds[1].fd = 3; // 监听fd = 3的文件
    fds[1].events = POLLOUT; // 希望监听发生写事件(输出事件)
    

    poll监听时, 如果没有动静就阻塞; 有动静不再阻塞, 并返回有动静的fd数量.
    注意:通常events不会设为POLLOUT(输出事件), 因为输出事件一般不会阻塞.

    如何判断哪些fd有动静?
    通过判断文件描述符 "期望监听发生的事件" == "实际事件", 说明希望的事件已经发生, 就可以对相应fd进行读写操作.

    // 判断集合中第1个监听的事件是否发生
    if (fds[1].events = fds[1].revents) {
      read(fds[1].fd, buf, sizeof buf);// 读fds[1].fd
    }
    
    1. nfds 数组元素个数(第一个参数fds[]数组大小)
    2. timeout poll阻塞超时时间
    • -1 不设置超时时间. 如果集合没有动静,就一直阻塞; -- 注意: 并不是设置为0表示不舍超时时间
    • !-1 单位是ms,如3000,表示超时时间设置为3s(即3000ms)

    返回值

    1. -1 函数调用失败, errno设置
    • 如果被信号中断导致出错返回-1, errno被设置为EINTR;
    • 如果不想被中断, 可以重新调用poll, 或者忽略/屏蔽可能的中断信号;
    1. 0 超时时间到, 并且没有文件描述符有动静.

    2. 0 返回有动静的文件描述符的数量. 下一步代码就需要通过遍历集合数组, 从中判断有动静的fd并读取数据

    示例

    完整代码见poll使用示例, 文件名: poll.c

    异步IO

    前面同时读键盘/鼠标的方法: 多进程, 多线程, 将读鼠标和读键盘设为非阻塞, 多路IO(select和poll), 都是主动读取. 但是对于read函数并不确定一定有数据, 如果有数据就能读取到, 如果没数据就阻塞.

    异步IO原理是, 底层数据准备OK后, 驱动/内核给进程发送一个"异步通知的信号"通知进程, 表示数据准备好了, 然后调用信号处理函数读取数据. 数据没有准备好时, 进程可以忙自己的事情.

    如, 用异步IO读鼠标数据时, 底层鼠标驱动准备好数据后, 会发一个"SIGIO"(异步通知信号)给进程, 进程调用捕获函数读取鼠标数据. 不过, SIGIO捕获函数需要自定义.

    使用异步IO方式读取鼠标和键盘

    • 步骤:
    1. 进程设置SIGIO信号捕获函数, 在捕获函数内读取鼠标数据;
    2. 进程设置鼠标驱动, 告诉驱动发送的SIGIO信号由当前进程接收;
    3. 进程设置读鼠标为异步IO方式;
    4. 进程阻塞读取键盘数据时, 如果鼠标没数据, 进程不关心鼠标; 如果有数据, 底层鼠标驱动会向进程发送一个SIGIO信号;
    5. 进程调用注册的SIGIO信号捕获函数读取鼠标数据;
    • 同步IO与异步IO:
      同步IO: 请求读取IO数据, 如果没有数据, 则阻塞直到有数据; 如果有数据, 则直接读取并返回;
      异步IO: 有数据时, 硬件驱动/内核通过发送信号通知进程读取数据; 没有数据时, 进程可以做自己的任务, 而不用阻塞.

    • 使用异步IO的2个前提:

    1. 底层驱动必须有相应的发送SIGIO信号的代码, 底层准备好数据后, 才会发送SIGIO信号给进程;
    2. 应用层必须进行相应的异步IO设置, 否则无法使用异步IO
      应用层对异步IO的设置, 通过fctnl()完成.

    BSD异步IO, 应用层设置:

    1. 调用signal/sigaction对设置SIGIO信号的捕获函数;
    2. 用fcntl F_SETOWN将接收SIGIO信号的进程设为当前进程;
    3. 使用fcntl F_SETFL, 对文件描述符增设O_ASYNC状态标识, 让fd支持异步IO
      例, 使用fcntl 为文件状态标识添加O_ASYNC选项

    示例: 异步IO读取鼠标数据
    完整代码见异步IO例程, 文件名:async.c

    int mousefd = -1;
    // 在SIGIO信号捕获函数中, 异步IO方式读取鼠标数据
    void signal_fun(int signo){
      if (signo == SIGIO) {
        int buf = 0;
        read(mousefd, &buf, sizeof buf);
      }
    }
    
    int main() {
      mousefd = open("/dev/input/mouse1", O_RDONLY); // 实际是mouse?, 取决于实际情况, 需要实测
    
      // 注册SIGIO捕获函数
      signal(SIGIO, signal_fun);
    
      // 告诉鼠标驱动, 当前进程接收SIGIO--很重要, 如果不设置进程不会捕获SIGIO
      fcntl(mousefd, F_SETOWN, getpid());
    
      // 鼠标文件描述符添加O_ASYNC, 为读取鼠标添加异步支持
      flag = fcntl(mousefd, F_GETFL); // F_GETFL 表明读取mouse打开文件描述符
      flag |= O_ASYNC // 添加O_ASYNC 异步IO标志
      fcntl (mousefd, F_SETFL, flag);
    
      // 告诉鼠标驱动当前进程捕获SIGIO信号
      fcntl(mousefd, F_SETOWN, getpid());
    
      while(1) {
      // 阻塞读取键盘数据, 略
      ...
      }
    }
    

    存储映射

    普通文件读写方式的缺陷

    调用read/write对普通文件进行读写, 由于底层封装多层, 在面对频繁读写大量数据时, 效率低下. 这就引入了存储映射.

    存储映射mmap

    存储映射(memory map)简称mmap, 是直接将实际存储的物理地址映射到进程空间, 而不使用read/write 函数. 对普通文件存储, 是硬盘地址映射到进程空间. 这样, 省去中间繁杂调用过程, 快速对文件进行大量输入输出.

    注意: 使用mmap时, open 不可省, read/write 可省.

    存储映射 vs 共享内存

    存储映射mmap原理类似于System V共享内存, 不过用途还是有区别:

    1. 共享内存
      主要应用进程间通信, 2个进程的进程工具映射到同一段RAM区域, 从而进行通信.
      共享内存映射到的物理地址是RAM地址.

    2. 存储映射
      mmap主要用于对文件进行大数据量的高效输入输出.
      mmap 映射到的物理地址是硬盘地址.

    mmap函数

    原型:

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

    功能:
    将文件所在的磁盘空间映射到进程空间.

    返回值:
    调用成功, 则返回映射的起始虚拟地址; 失败则返回(void *)-1, errno设置.

    参数:

    1. addr 指定映射的起始虚拟地址
      如果addr = NULL, 表示由内核决定映射的起始虚拟地址 -- 常用
      如果addr ≠ NULL, 需要自己手动指定, 起始地址必须是虚拟页(4K,一页大小)的整数倍, 类似于指定共享内存shmat的映射起始地址.

    2. length 要映射的文件长度

    3. prot 映射区的访问权限, 通过宏:
      1)PROT_EXEC 映射区的内容可执行, 如果映射的文件是一个可执行文件, 可将映射权限指定为PROT_EXEC;

    2)PROT_READ 可读;

    3)PROT_WRITE 可写, 如果指定了该项, 文件必须以O_RDWR方式打开, 而不能以O_RDONLY或O_WRONLY方式打开;
    上面3种操作, 可以进行 "|" 操作, 如PROT_EXEC | PROT_READ (可读可执行).

    4)PROT_NONE 映射区不允许方法(不可读,写,执行), 一般不会指定该项;

    1. flags 如果向映射区写入了数据, 是否将数据立即更新到文件中
      1)MAP_SHARED 进程映射空间可以共享给其他进程, 对共享区的改变对其他进程可见
      2)其他设置, 略

    2. fd 需要被映射文件的描述符

    3. offset 表示从文件头offset处开始映射. 一般指定为0, 表示从文件头开始映射

    munmap函数

    原型:

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

    功能:
    取消映射

    返回值:
    调用成功返回0, 失败返回-1, errno设置.

    参数:

    1. addr 映射的起始虚拟地址;
    2. length 需要取消的长度

    mmap示例

    利用存储映射将一个文件拷贝到另外一个文件
    mmap例程, 文件名: mmap.c

        /* 打开源文件 */
        int srcfd = open("./srcfile", O_RDONLY);
        if (srcfd < 0) print_err("open source file fail", __LINE__, errno);
    
        /* 打开目标文件 */
        int dstfd = open("./dstfile", O_RDWR | O_CREAT | O_TRUNC, 0664);
        if (dstfd < 0) print_err("open dest file fail", __LINE__, errno);
    
        /* 获取源文件状态属性 */
        struct stat statbuf = {0};
        fstat(srcfd, &statbuf);
        int len = statbuf.st_size; // 文件总大小
    
        /* 映射源文件, 返回映射到的虚拟空间首地址 */
        void *srcaddr = mmap(NULL, len, PROT_READ, MAP_SHARED, srcfd, 0);
        if (srcaddr == (void *)-1) print_err("mmap dstfile fail", __LINE__, errno);
    
        /* 映射目标文件, 返回映射到的虚拟空间首地址 */
        ftruncate(dstfd, len); // 扩展目标文件到源文件大小,因为mmap无法映射到长度为0的文件
        void *dstaddr = mmap(NULL, len, PROT_WRITE, MAP_SHARED, dstfd, 0);
        // MAP_SHARED 与其他进程共享映射区域 
        // 如果设置了PROT_WRITE, 映射的文件必须以O_RDWR方式打开,不能以O_RDONLY或O_WRONLY
    
        if (dstaddr == (void *)-1) print_err("mmap dstfile fail", __LINE__, errno);
    
        /* 将源文件数据复制到目标文件 
         * mempcy 要求两个地址段无重叠区域, 否则可能产生脏数据
         * */
        memcpy(dstaddr, srcaddr, len);
    

    参考

    《Linux系统编程、网络编程》

  • 相关阅读:
    nopcommerce商城系统--源代码结构和架构
    nopcommerce商城系统--如何编写一个插件
    ASP.NET MVC:通过 FileResult 向 浏览器 发送文件
    【js与jquery】电子邮箱、手机号、邮政编码的正则验证
    也用 Log4Net 之走进Log4Net (四)
    也用 Log4Net 之将自定义属性记录到文件中 (三)
    也用 Log4Net 之将日志记录到数据库的后台实现 (二)
    也用 Log4Net 之将日志记录到数据库的配置 (一)
    log4net 将日志写入数据库
    ASP.NET MVC 3 入门级常用设置、技巧和报错
  • 原文地址:https://www.cnblogs.com/fortunely/p/14751139.html
Copyright © 2020-2023  润新知