Linux 系统编程学习笔记 - 高级IO
概述
主要高级IO:
- 非阻塞IO
- 记录锁(文件锁)
- IO多路复用(I/O multiplexing)
- 异步IO
- 存储映射
高级IO,涉及到文件的IO操作,必然会用到文件描述符(fd),而且依赖于fcntl函数支持。
非阻塞IO
阻塞读文件
当读某些文件时,如果文件没有数据,会导致读操作阻塞,如:
- 读鼠标/键盘等字符设备文件;
- 读管道文件(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. 读读共享
某个进程读数时,其他进程也读取,但是不会破坏数据完整性,无需担心数据相互干扰问题。
信号量的局限性
多个进程同时读写文件的情形,使用信号量只会每种情形都互斥,难以实现读读共享。因为信号量并不直接识别线程操作是读操作,还是写操作。而使用文件锁,可用既做到互斥,又能做到共享。
使用文件锁加锁
读锁 & 写锁
对文件加锁可用分为两种锁:读文件锁(简称读锁),写文件锁(简称写锁)。
读锁、写锁之间关系:
- 读锁和读锁共享:可用重复加读锁,别人加了读锁在没有解锁前,我们依然可用继续加读锁;
- 读锁,写锁互斥:别人加读锁没有解锁前,我们加写锁会失败;别人加写锁,我们加读锁会失败。
- 写锁,写锁互斥:别人加了写锁没解锁前,我们不能加写锁,加写锁会失败;
加锁失败后2种处理方式:
- 阻塞加锁 - 阻塞,直到别人解锁,然后我们加锁成功;(常用)
- 非阻塞加锁 - 出错返回,不阻塞;
文件锁的加锁方式
1. 对整个文件内容加锁
最常用方式是对整改文件加锁。如果文件长度变化,加锁内容的长度也会自带变化。
2. 对文件部分内容加锁
对文件加区域锁,即对文件指定区域范围内容加锁。一般地,对多少内容加锁,就对多少内容解锁。
文件锁的实现
需要用到fcntl。
fcntl
fctnl函数原型
#include <unistd.h>
#include <fcntl.h>
// 第三个参数...,变参数,用到时才写
int fcntl(int fd, int cmd, .../* struct flock *flock */);
-
功能
cmd设置为与文件锁相关宏时(见下参数描述),fcntl用于实现文件锁。 -
返回值
成功返回0,失败-1,errno被设置。 -
参数
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);
-
功能
按operation要求,对fd所指文件加对应文件锁。 -
返回值
成功返回0;失败-1,设置errno。 -
参数
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个阻塞字符输入设备)的方法:
- 多进程;
- 多线程;
- 将“读鼠标”和“读键盘”设置为非阻塞实现;
- 多路IO;
多路IO工作原理
多路IO工作原理如下图所示,
注意:
- 只有读操作阻塞的fd,用多路IO才有意义;
- 休眠时,监听机制依然有效,能监听到有数据到来;
多路IO的优势
针对类似于同时读取鼠标/键盘数据的情况而言,
-
多进程
开销太大,不建议使用。 -
非阻塞方式
需要搭配while循环不断轮询,耗费CPU资源,不建议使用。 -
多线程
开销较低,常用方法 -
多路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
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数量.
参数
- 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
}
- nfds 数组元素个数(第一个参数fds[]数组大小)
- timeout poll阻塞超时时间
- -1 不设置超时时间. 如果集合没有动静,就一直阻塞; -- 注意: 并不是设置为0表示不舍超时时间
- !-1 单位是ms,如3000,表示超时时间设置为3s(即3000ms)
返回值
- -1 函数调用失败, errno设置
- 如果被信号中断导致出错返回-1, errno被设置为EINTR;
- 如果不想被中断, 可以重新调用poll, 或者忽略/屏蔽可能的中断信号;
-
0 超时时间到, 并且没有文件描述符有动静.
-
0 返回有动静的文件描述符的数量. 下一步代码就需要通过遍历集合数组, 从中判断有动静的fd并读取数据
示例
异步IO
前面同时读键盘/鼠标的方法: 多进程, 多线程, 将读鼠标和读键盘设为非阻塞, 多路IO(select和poll), 都是主动读取. 但是对于read函数并不确定一定有数据, 如果有数据就能读取到, 如果没数据就阻塞.
异步IO原理是, 底层数据准备OK后, 驱动/内核给进程发送一个"异步通知的信号"通知进程, 表示数据准备好了, 然后调用信号处理函数读取数据. 数据没有准备好时, 进程可以忙自己的事情.
如, 用异步IO读鼠标数据时, 底层鼠标驱动准备好数据后, 会发一个"SIGIO"(异步通知信号)给进程, 进程调用捕获函数读取鼠标数据. 不过, SIGIO捕获函数需要自定义.
使用异步IO方式读取鼠标和键盘
- 步骤:
- 进程设置SIGIO信号捕获函数, 在捕获函数内读取鼠标数据;
- 进程设置鼠标驱动, 告诉驱动发送的SIGIO信号由当前进程接收;
- 进程设置读鼠标为异步IO方式;
- 进程阻塞读取键盘数据时, 如果鼠标没数据, 进程不关心鼠标; 如果有数据, 底层鼠标驱动会向进程发送一个SIGIO信号;
- 进程调用注册的SIGIO信号捕获函数读取鼠标数据;
-
同步IO与异步IO:
同步IO: 请求读取IO数据, 如果没有数据, 则阻塞直到有数据; 如果有数据, 则直接读取并返回;
异步IO: 有数据时, 硬件驱动/内核通过发送信号通知进程读取数据; 没有数据时, 进程可以做自己的任务, 而不用阻塞. -
使用异步IO的2个前提:
- 底层驱动必须有相应的发送SIGIO信号的代码, 底层准备好数据后, 才会发送SIGIO信号给进程;
- 应用层必须进行相应的异步IO设置, 否则无法使用异步IO
应用层对异步IO的设置, 通过fctnl()完成.
BSD异步IO, 应用层设置:
- 调用signal/sigaction对设置SIGIO信号的捕获函数;
- 用fcntl F_SETOWN将接收SIGIO信号的进程设为当前进程;
- 使用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共享内存, 不过用途还是有区别:
-
共享内存
主要应用进程间通信, 2个进程的进程工具映射到同一段RAM区域, 从而进行通信.
共享内存映射到的物理地址是RAM地址. -
存储映射
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设置.
参数:
-
addr 指定映射的起始虚拟地址
如果addr = NULL, 表示由内核决定映射的起始虚拟地址 -- 常用
如果addr ≠ NULL, 需要自己手动指定, 起始地址必须是虚拟页(4K,一页大小)的整数倍, 类似于指定共享内存shmat的映射起始地址. -
length 要映射的文件长度
-
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 映射区不允许方法(不可读,写,执行), 一般不会指定该项;
-
flags 如果向映射区写入了数据, 是否将数据立即更新到文件中
1)MAP_SHARED 进程映射空间可以共享给其他进程, 对共享区的改变对其他进程可见
2)其他设置, 略 -
fd 需要被映射文件的描述符
-
offset 表示从文件头offset处开始映射. 一般指定为0, 表示从文件头开始映射
munmap函数
原型:
#include <sys/mman.h>
int munmap(void *addr, size_t length);
功能:
取消映射
返回值:
调用成功返回0, 失败返回-1, errno设置.
参数:
- addr 映射的起始虚拟地址;
- 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);