Linux 系统编程学习笔记 - 高级IO

概述

主要高级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");
    n = read(stdin, s, sizeof s);
    // n = read(mousefd, &coordinate, sizeof coordinate);
    if (n > 0) printf("%s\n", s);
    printf("You have input data\n");
  }
}

问题:
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\n", 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, "\n", 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, "\n", 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\n", buf);
        }

        if (FD_ISSET(mousefd, &readfds)) {
            mousepos = 0;
            int readret = read(mousefd, &mousepos, sizeof(mousepos));
            if (readret > 0) printf("%d\n", mousepos);
        }
    }
    else if (ret == 0) { // select 超时
        printf("time out\n");
    }

}

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系统编程、网络编程》

posted @ 2021-05-11 22:31  明明1109  阅读(424)  评论(0编辑  收藏  举报