同步、异步,阻塞和非阻塞等概念
同步和异步
- 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
阻塞与非阻塞
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
所以说,同步和异步针对的是消息通知机制,我要得到这个消息是自己一直等待还是被调用者在处理完后通知调用者。而阻塞非阻塞是针对本身线程或进程来说。线程或进程要一直等待则是阻塞,可以不等待处理其他任务为非阻塞。
以下来源网络:
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
- 老张把水壶放到火上,立等水开。(同步阻塞)
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
- 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
- 老张把响水壶放到火上,立等水开。(异步阻塞)老张觉得这样傻等意义不大
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)。
本篇索引:
- 引言
- 非阻塞IO
- 记录锁(文件锁)
- io多路复用(I/O multiplexing )
- 异步IO
- 存储映射IO
1 引言
我们第三篇学习了对IO的open、read、write等的操作,这一篇我们将会学习对IO的一些高级操作,实际上这一篇的内容是对第三篇内容的进一步升华,主要的内容如下:
- 非阻塞IO:对文件实现非阻塞操作。
- 记录锁:利用文件实现锁的机制。
- IO多路复用:实现单线同时操作多个阻塞IO,分select和poll两种的操作。
- 存储映射IO:mmap
本篇不重理论,重点是列举各种例子代码,教会大家如何使用这些高级IO的设置和使用。
2 非阻塞IO
2.1 低速系统调用之阻塞
所有系统调用被分为两类,一类是低速系统调用,另一类是其它系统调用。
1)低速系统调用:可能会使进程永远阻塞的一类系统调用,系统调用导致进程阻塞的原因有两种。
a)函数操作文件时,因文件类型而阻塞,阻塞与函数本身无关
读某些文件:由于数据不存在会导致调用者永远阻塞
- 读管道文件:管道是进程间通信用的特殊文件,读管道时,如果管道中并无数据会导致对管道的读操作会阻塞。
- 读终端设备:如读鼠标、键盘等字符设备类文件,以及网络设备文件时,如果没有数据的话,读操作也会阻塞。
- 注意:值得强调的是低速系统调用读磁盘I/O(普通文件)并非是阻塞的,如果有数据会带着数据正常返回,如果没有数据则也会返回,所以不会阻塞。
写某些文件:在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致写操作长期的阻塞下去。
打开文件:在某些条件成立之前可能有些文件是无法打开的,这同样可能会导致打开操作长期的阻塞下去。
- 情况1:如果想要成功打开某些终端设备,那么你就必须等到某些调制解调器应答后才能打开,否者会一直阻塞下去。
- 情况2:如果某个管道的读端没打开时,而你又想以写方式打开该管道,那么这个以写 方式打开的操作会一直阻赛直到某个地方以读打开这个管道为止。
b)某些函数本身就是阻塞的
pause函数,wait函数,sleep函数,某些ioctl操作,以及某些进程间通信的函数(如当消息队列的消息接受函数设置了阻塞时),这些函数调用本身就是阻塞的。
2)其它系统调用:略
2.2 如何设置和修改阻塞为非阻塞
前面说过,某些文件默认打开后默认对文件的操作就是阻塞的,但是利用对文件描述符设置,可将其操作设置为非阻塞的,主要的方法有如下两种。
1)打开文件时指定非阻塞,例子如下:
以非阻塞的方式打开标准输入文件。
int main(void)
{
int fd = -1;
fd = open("/dev/stdin", O_RDONLY|O_NONBLOCK);
if(fd < 0)
{
perror("open stdin is fail");
exit(-1);
}
return 0;
}
/dev/stdin是标准输入文件,对应着键盘输入,一般情况下默认就是以阻塞方式打开的,但如果我们在打开时指定O_NONBLOCK参数的话,就指定为了非阻塞,当我们去read该文件时就不会再阻塞了。
2) 用fcntl函数进行设置
上例中我们重新打开了标准输入文件,新返回的描述符(3)和描述符0同时指向了标准输入文件,虽然fd被设置为了非阻塞,但是描述符0任然是阻塞的。因为0和3这两个文件描述符是分别各自调用open函数打开/dev/stdin文件而返回得到的,这种情况下各个文件描述符指向的文件结构关系如下:
从上图我们很明显的看到0,3这两个描述符各自有一个文件表,可以设置自己的文件状态标志,所以0是阻塞的而3却是非阻塞的就不难理解了。那么如何将已经打开了的文件描述符设置为非阻塞的呢?这就又要使用到fcntl函数了,比如我们可以将已经打开了的0设置为非阻塞,代码实现如下:
int main(void)
{
int fd = -1, flag = -1;
/* F_GETFL:获取描述符原有状态给flag的命令,目的是为了保护原有的状态
* STDIN_FILENO:指向标准输入文件的文件描述符0 */
flag = fcntl(STDIN_FILENO, F_GETFL);
flag |= O_NONBLOCK;//将原有文件状态 | 非阻塞标志
//将修改后的包含了非阻塞标志的新状态重新设置回去
fcntl(STDIN_FILENO, F_SETFL, flag);
return 0;
}
2.3 非阻塞举例
1)同时阻塞地读键盘和读鼠标
我们在一个单进程里面同时实现键盘的输入和鼠标的输入,但是对于这两个低速系统调用默认情况的读都是阻塞的,所以这两个的输入会相互阻塞,如下例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
// 读取鼠标
int fd = -1;
char buf[200];
int ret = -1;
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
while (1)
{
// 读鼠标
memset(buf, 0, sizeof(buf));
//printf("before 鼠标 read.\n");
ret = read(fd, buf, 50);
if (ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buf);
}
// 读键盘
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
ret = read(0, buf, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buf);
}
}
return 0;
}
该程序必须在超级用户下运行,因为鼠标文件只能在超级用户下才能被打开,运行程序时由于先读的是鼠标,它阻塞了键盘,所以我们先输入键盘是没有用的,当从鼠标输入数据后,鼠标数据打印出来,这时进程又阻塞在了读键盘处,所以此时从鼠标输入数据是没有用的,这时必须敲键盘输入数据,才能回到读鼠标处,由于鼠标数据是整形的坐标值,所打印出来是乱码。
2)非阻塞地实现读键盘和鼠标
从前面我们知道,键盘和鼠标的读导致了相互的阻塞,我们输入时并不通畅,现在我们将它们都改为非阻塞的,那么它们就不会相互阻塞,输入就会变得通畅,对上例修改后的代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 读取鼠标
int fd = -1;
int flag = -1;
char buf[200];
int ret = -1;
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
perror("open:");
return -1;
}
// 把0号文件描述符(stdin)变成非阻塞式的
flag = fcntl(0, F_GETFL); // 先获取原来的flag
flag |= O_NONBLOCK; // 添加非阻塞属性
fcntl(0, F_SETFL, flag); // 更新flag
// 这3步之后,0就变成了非阻塞式的了
while (1)
{
// 读鼠标
memset(buf, 0, sizeof(buf));
//printf("before 鼠标 read.\n");
ret = read(fd, buf, 50);
if (ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buf);
}
// 读键盘
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
ret = read(0, buf, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buf);
}
}
return 0;
}
经过这么设置后,鼠标和键盘不再相互阻塞,所以运行这个程序时不必再忌讳谁先输入的问题了,谁先输入都可以。只是这种非阻塞会导致进程时刻都处在循环的,这种轮询的机制会非常的消耗cpu资源,为了解决同时输入键盘和鼠标的问题,这并不是一个好的解决方法,我们前面学过了多进程,所以我们可以利用两个进程来时实现同时读鼠标和键盘,假如A进程读鼠标,B进程读键盘,虽然它们都是阻塞的,但确是各自阻塞各自的,它们互不相干扰。
3)利用两个进程实现同时读键盘和鼠标
我们开两个进程,一个进程读键盘,一个进程读鼠标,由于进程本身就是迸发同时向前运行的,所以这里再也不需要将键盘和鼠标设置为非阻塞。例子的例子如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int fd = -1, ret = -1;
char buf[200];
pid_t pid;
/* 开来两个进程,父进程读鼠标,子进程读键盘 */
pid = fork();
if (pid == 0)
{
while (1)
{
// 读键盘
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
ret = read(0, buf, 5);
if (ret > 0)
{
printf("键盘读出的内容是:[%s].\n", buf);
}
}
}
else if (pid > 0)
{
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
while (1)
{
// 读鼠标
memset(buf, 0, sizeof(buf));
//printf("before 鼠标 read.\n");
ret = read(fd, buf, 50);
if (ret > 0)
{
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
}
return 0;
}
例子中并没有将键盘和鼠标都设置为非阻塞的,但同样能够实现同时键盘和鼠标,但当后面我们学到了select、poll多路io机制和异步通知后,还可以用这些方法来解决同时读鼠标和键盘的问题。
3 记录锁(文件锁)
3.1 为什么需要记录锁
当多个进程试图对同一个文件都进行读写操作时,如下图所示:
我们肯定希不同进程之间各自的读写操作望满足如下条件,以保护各个进程向文件所写之数据不被篡改。
- 当某个进程正在写文件时,其它所有进程肯定不能写,否者会相互篡改。
- 当某个进程正在写文件时,其它所有进程不能够读,因为别人在没有写完之前读出的数据是不完整的。
- 当某个进程正在读时,其它所有的进程都可以共享的读,因为读不会造成数据篡改。
- 当某个进程正在读时,其它所有的进程都不可以写,否者会导致读出的数据不完整。
总结以上几点就是:
- 写与写之间互斥
- 读与写之间互斥
- 读与读之间共享 //读不会可以共享
为了实现按照上述保护方式对文件进行读写,我们引入了记录锁,记录的设置也需要用到fcntl函数,这个函数我们在第3篇时就已经学习过了,当时说过该函数有很多的功能,今天我们就学习如何利用fcntl函数设置记录锁。
记录锁:利用文件描述fd实现对其指向的文件内容进行加锁,记录锁又称区域锁。
- 对整个文件内容加锁
- 对文件某部分内容加锁
记录锁主要使用目的:
- 保护整个文件或文件某区域的内容。
- 并不需要保护文件内容,只是借用文件记录锁,以实现加锁(例如保证原子操作)。
记录锁的种类:
- 建议性记录锁,本篇重点
- 强制性记录锁,不讨论
准确来讲,对整个文件加锁的记录锁应该被称为文件锁,只对文件部分内容加锁的才应被称为记录锁或区域锁,但通常情况下我们并不对这两个名称加以区别,都统称为记录锁或文件锁。
3.2 fcntl函数设置建议性记录锁
1)函数原型和所需头文件
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
2)函数功能:该函数有多种功能,在这里我们主要讨论如何利用该函数进行设置记录锁。
3)函数参数
- int fd:文件描述符,指向需要加锁的文件。
- int cmd:设置记录锁时,cmd有三种设置,F_GETLK,F_SETLK,F_SETLKW。
- 第三个参数:当设置记录锁时,为struct flock *flockptr,一个指向struct flock 结构体的指针,该结构体中设置好了我们需要设置的记录所的各个参数。
4)函数返回值 设置记录锁成功返回0,失败则返回-1, 并且errno被设置。
5)注意
(1)fcntl函数是一个变参函数,一般情况下只需设置前两个参数,但是设置记录锁时,fcntl函数需要使用到第三个参数。
(2)F_GETLK、F_SETLK和F_SETLKW含义如下:
- F_GETLK:决定由flockptr所描述的锁是否被另外一把锁所排斥(阻塞)。如果存在 一把锁,它阻止创建由flockptr所描述的锁,则这把现存的锁的信息写到flockptr指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中其他信息保持不变。
- F_SETLK:设置由flockptr所描述的锁。如果试图建立一把按上述兼容性规则并不允许的锁,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
- F_SETLKW:这是FSETLK的阻塞版本,命令名中的W表示等待(wait)。如果由于存在其它锁,那么按兼容性规则由flockptr所要求的锁不能被创建,那么调用进程睡眠。 如果捕捉到信号则睡眠中断(可以手动重启这个系统调用)。如此直到设置成功为止。
(3)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 offset for lock
off_t l_len; //Number of bytes to lock
pid_t l_pid; //PID of process blocking our lock(F_GETLK only)
}
结构体成员说明
①short l_type:记录锁类型
- F_RDLCK:读锁(或称共享锁)
- F_WRLCK:写锁
- F_UNLCK:解锁
②short l_whence:加锁位置粗定位,设置同lseek的whence
- SEEK_SET:文件开始处
- SEEK_CUR:文件当前位置处
- SEEK_END:文件末尾位置处
③off_t l_start:精定位,相对l_whence的偏移,设置同lseek的offset。
④off_t l_len:文件中需被加锁区域的字节数,当设置为0时,表示加锁到文件末尾。
⑤pid_t l_pid:加锁进程的PID,仅对F_GETLK有用。
(4)加锁区域的起点可以是文件的尾端或超过文件尾端的位置,可以是文件的起始位置, 但是绝不能在文件起始位置前设置。
(5)如若l_len为0,则表示锁的区域从其起点(起点由lstart和lwhence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。每当项文件写入新的内容后,加锁区域会自动延伸到文件新的尾端。
(6)为了锁整个文件,通常将l_start设置为0,l_whence设置为SEEK_SET,l_len设置为0。
(7)可实现对文件组合加锁,比如对文件0-100字节加读锁,对101-250加写锁。
(8)可实现对文件部分区域解锁,例如文件0-500字节都被加了写锁,但解锁时可只解300-500解锁,而0-299区域的锁将任然存在。
6)测试用例
设置记录锁--记录锁的设置比较繁琐,为了避免麻烦,我们只写一个相关函数,这个函数在自定义的头文件record_lock.h中实现,那么如何调用该函数实现非阻塞地加读锁、写锁,或阻塞的加读锁、写锁以及解锁则由不同的宏来实现,实现该函数和宏的头文件如下:
/* fcntl函数的包装,带w表示阻塞调用 */
#include <unistd.h>
#include <fcntl.h>
#define read_lock(fd, l_whence, l_offset, l_len) lock_fun(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len)
#define read_lockw(wfd, l_whence, l_offset, l_len) lock_fun(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len)
#define write_lock(fd, l_whence, l_offset, l_len) lock_fun(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len)
#define write_lockw(wfd, l_whence, l_offset, l_len) lock_fun(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len)
#define unlock(fd, l_whence, l_offset, l_len) lock_fun(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len)
int lock_fun(int fd, int cmd, int l_type, int l_whence, off_t l_offset, off_t l_len)
{
struct flock f_lock;
f_lock.l_type = l_type;
f_lock.l_whence = l_whence;
f_lock.l_start = l_offset;
f_lock.l_len = l_len;
return(fcntl(fd, cmd, &f_lock));
}
测试记录锁--测试记录锁的实现同上,只有一个函数,但多种不同测试都由不同的宏实现。
/* fcntl函数需要用到的头文件 */
#include <unistd.h>
#include <fcntl.h>
#define testlock(fd, l_whence, l_offset, l_len) test_lock(fd, l_whence, l_offset, l_len)
int test_lock(int fd, int l_whence, off_t l_offset, off_t l_len)
{
int ret = 0;
struct flock flock = {0};
flock.l_whence = l_whence;
flock.l_start = l_offset;
flock.l_len = l_len;
ret = fcntl(fd, F_GETLK, &flock);
if(ret < 0)
{
perror("in test_lock fcntl is fail");
exit(-1);
}
else if(F_RDLCK == flock.l_type) printf("%d seted read_lock\n", flock.l_pid);
else if(F_WRLCK == flock.l_type) printf("%d seted write_lock\n", flock.l_pid);
else if(F_UNLCK == flock.l_type) printf("unlock\n");
return 0;
}
在使用记录锁前,大家请看下面这个例子:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
int ret = -1, fd = -1;
fd = open("./file", O_CREAT | O_RDWR, 0664);
if (fd < 0)
{
perror("open ./file is fail");
exit(-1);
}
/* 父子进程并发的向file文件里写hello worrd\n */
ret = fork();
if (ret == 0) // 子进程
{
while (1)
{
write(fd, "hello ", 6);
write(fd, "world\n", 6);
}
}
else if (ret > 0) // 父进程
{
while (1)
{
write(fd, "hello ", 6);
write(fd, "world\n", 6);
}
}
return 0;
}
运行结果:
我们发现结果中居然出现了hello hello world的情况,而导致这个情况的原因是因为write(fd, "hello ", 6)和write(fd, "world\n", 6)的这两个操作并不是一个原子操作,假当父进程刚写入“hello ”之后,父进程就被立即切换到子进程,子进程紧接着就也写“hello ”,就会导致这样的结果,分析入下图所示:
这对write(fd, "hello ", 6)和write(fd, "world\n", 6)不是原子操作的问题,我们可以利用我们的记录所来改进,改进后的代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include "record_lock.h"
int main(void)
{
int ret = -1, fd = -1;
fd = open("./file", O_CREAT | O_RDWR, 0664);
if (fd < 0)
{
perror("open ./file is fail");
exit(-1);
}
/* 父子进程并发的向file文件里写hello world\n */
ret = fork();
if (ret == 0) // 子进程
{
while (1)
{
write_lockw(fd, SEEK_SET, 0, 0);//对整个文件加锁
write(fd, "hello ", 6);
write(fd, "world\n", 6);
unlock(fd, SEEK_SET, 0, 0);//解锁
}
}
else if (ret > 0)
{
// 父进程
while (1)
{
write_lockw(fd, SEEK_SET, 0, 0);// 对整个文件加锁
write(fd, "hello ", 6);
write(fd, "world\n", 6);
unlock(fd, SEEK_SET, 0, 0);//解锁
}
}
return 0;
}
运行修改后的代码(留意黑体部分),然后vim file,发现再也没有“hello hello world”的现象了,这是因为加了记录锁后,write(fd, "hello ", 6)和write(fd, "world\n", 6)变成了原子操作,分析如下图:
从上图的中可以看出,write(fd, "hello ", 6)和write(fd, "world\n", 6)被强制成为了一个原子操作,进程B在进程A执行完write(fd, "hello ", 6)和write(fd, "world\n", 6)之前是不会加锁成功的,会一直阻塞下去直到进程A执行完成write(fd, "hello ", 6)和write(fd, "world\n", 6)并成功的解锁之后,进程B才能加锁成功。
3.3 记录锁讨论
3.3.1 记录锁的实现
观察上图得到:
1)当同一进程中多个文件描述符指向同一文件时,只要其中的任何一个文件描述符被关闭,那么该进程加在文件上的记录锁将 会被关闭,因为同一个文件不管被打开或复制多少次,但是它们共享却只有一个锁链表。
2)进程终止时(不管是正常或异常终止),该进程所加的记录锁全部被释放。
3)当某个进程相想对某个文件加锁时,会首先检查锁链表。
- a)如果发现已经被加了一个读锁,该进程可加读锁,但是不可以加写锁。
- b)如果发现已经被加了一个写锁,该进程不能加写锁,也不能加读锁。
4)fork产生的子进程不会继承父进程所加的锁,因为锁的作用是阻止多个进程同时写同一个文件(或同一文件区域) 。如果子 进程继承父进程的锁,则父、子进程就可以同时写同一个文件。如果子进程想加锁,必须自己重新调用fcntl函数重新加锁。
5)在执行exec后,新程序可以继承原执行程序的锁。
6)同意进程加多个
3.4 强制性记录/文件锁(仅做了解)
3.4.1 为什么需要强制性锁
我们前面讲的锁都是建议性锁,对于建议性锁存在一个问题,那就是当多个进程对文件进行读写操作,这些进程设置的记录锁,在相互之间是起作用的,但如果这时有一个除了这几个进程(该进程没有对该文件加锁)外另一个进程也去写这个文件,那么其他进程锁加的锁对这一个进程是没有任何作用的,那么该文件内容就会被这个进程的写操作所篡改,很多情况下我们是不希望出现这种情况的,针对这种情况我们就需要设置强制性的记录锁。
3.4.2、强制性记录性的加锁和启动
- 加锁方式:同建议性记录锁
- 如何启动强制性记录锁?
对会被加锁的文件打开其设置-组-ID位,关闭其组-执行位,如此就对该文件启动了强制性锁机制。
3.4.3 强制性记录锁对其它进程的影响
如果一个进程试图读、写一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了读、写锁,此时会发生什么呢?对这一问题的回答取决于三方面的因素:
- a)操作类型(read或write)
- b)其它进程保有的锁的类型(读锁或写锁)
- c)以及对该文件操作的有关描述符是阻塞还是非阻塞的。
如果一个进程试图open文件,而其它进程又对文件加了强制性锁,此时又会发生什么呢?
- 如果open的flag标识中设置了O_TRUNC,立即出错返回,errno被设置EAGAIN。对O_TRUNC情况出错返回是有意义的,因为其他进程对该文件持有读、写锁,所以不能将 其截短为0。
- 如果open的flag标识中设置了O_CREAT,立即出错返回,errno被设置EAGAIN。对OCREAT情况在返回时也设置errno则无意义,因为该标志的意义是如果该文件不存在则创建,由于其它进程对该文件持有记录锁,因而该文件肯定是存在的。
注意:并不是所有的linux操作系统都支持强制性记录锁。
4 io多路复用(I/O multiplexing )
前面我们为了实现同时读键盘和鼠标,采用非阻塞或多进程来实现,但是这两种方法都有一定的缺陷,非阻塞会需要轮询,很消耗cpu资源,如果采用多进程时,进程之间切换也是很耗费资源的,并且当程之间需要相互共享资源的话,这就需要加入进程间通信机制,这就会使得我们的程序变得更加的复杂。
对此我们引入一种新的解决办法,多路io复用,其分为如下两种:
- select机制
- poll机制
- epoll机制
不管是那种机制,多路io复用的是原理是一致的,其基本思想是构造一个文件描述符的表,在这个表里面存放了会阻塞文件描述符,然后调用多路复用函数,该函数每隔一段时间会去检查一次,看表中是否有某个或几个文件描述符有I/O操作,没有就休眠一段时间,再隔一段再去检查一次,如果其中的一个或多个文件名描述符有了动作,函数返回不在休眠,将分别对其有动作的文件描述符做相应操作。
- 该函数为阻塞函数;
- 该函数对文件描述符的检测操作是由内核来完成的;
- 在返回时,它告诉进程有多少select、poll机制(哪些epoll机制)描述符要进行I/O操作。
实际上多路复用本身也存在轮询检查过程,但是绝大部分时间却是在休眠,所以就避免了一般的轮询机制,以及多进程实现所带来的相当的资源损耗。多路复用原理如下图所示:
注意:一般情况下集合中都设置阻塞的文件描述符,设置非阻塞的描述符是没有意义的。外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。
4.1 select,pselect函数
1)函数原型和所需头文件
#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);
2)函数功能
- 这两个函数都是为了实现多路复用,但是这两个函数都能够被信号中断,但是pselect函数能够通过sigmask参数屏蔽掉那些我们不希望中断pselect系统函数的信号。
3)函数参数
a)select函数
- nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
- readfds: 监控有读数据到达文件描述符集合,传入传出参数
- writefds: 监控写数据到达文件描述符集合,传入传出参数
- exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
- timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
文件描述集操作函数
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
b)pselect函数
- 前四个参数:同select函数
- struct timespec *timeout:成员结构体如下:
struct timespec
{
long tv_sec; /* seconds(秒) */
long tv_nsec; /* nanoseconds (纳秒)*/
};
- 该参数填NULL表示不设置超时,这种情况下如果没有描述符响应,同时也没有信号中断的话则永远阻塞。
- 如果需要设置超时,则需填写设置了时间结构体的地址,如果没有描述符响应但设置的时间却到了,立即返回而不再阻塞。
- sigset_t *sigmask:信号屏蔽集,用于设置我们希望屏蔽的信号的,防止这些信号中断pselect的调用。
4) 函数返回值
- 返回-1:说明函数调用失败,errno被设置。
- 返回0:超时时间到并且没有一个描述符有响应,返回0说明没有一个描述符准备好。
- 返回值>0:返回有响应的文件描述符的数量。
5)注意
- 当内核检测到集合中有某个或几个文件描述符有响应时,这个集合将会被重新设置,用于存放那些有动作的文件描述符,所以一旦函数调用完毕我们必须重新设置这些集合。
- 当pselect被执行时,信号屏蔽字会被sigmask替换,相应的被设置了屏蔽的信号会被屏蔽,但是这个函数一旦执行完毕,信号屏蔽字会被还原。
- select和pselect都是会被信号中断的低速系统调用,当然我们可以手动重启该调用。
- 如果我们不希望select或pselect函数被信号中断,那么设置的方法如下:
- 忽略那些我们不希望的信号:(1)利用sigfilleset、sigdelset、sigpromask等函数,去修改信号屏蔽字以屏 蔽这些信号。(2)专门使用pselect函数,因为该函数调用期间,函数中的sigmask参数(设置了我们 希望屏蔽的信号)会去替换信号屏蔽字,被我们屏蔽的信号一旦发生后,就会成为未决信号而被设置到了未决信号集中,但是该函数一旦调用完毕,信号屏蔽字会被还原, 以前因被屏蔽而被设置在未决信号集中的未决信号就可能会被响应(响应方式:调用 捕获函数或终止进程)。
- 文件描述符集合中设置的都是会导致阻塞的描述符,设置非阻塞的文件描述符没有太大意义,所以集合中不要设置普通文件的描述符,因为不会阻塞。比如读普通文件时, 不管有没有数据,read函数都将返回。
- select可以用来模拟精度为us级别的定时器,而pselect则可以用来模拟精度为ns级别的定时器。
6)测试用例
select函数 多路复用读键盘和鼠标,打开鼠标需要超级用户权限
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
// 读取鼠标
int fd = -1, ret = -1;
char buf[200];
fd_set myset;
struct timeval tm;
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 当前有2个fd,一个是fd,一个是0
// 处理myset
FD_ZERO(&myset);
FD_SET(0, &myset);
FD_SET(fd, &myset);
tm.tv_sec = 10;
tm.tv_usec = 0;
ret = select(fd + 1, &myset, NULL, NULL, &tm);
if (ret < 0)
{
perror("select: ");
return -1;
}
else if (ret == 0)
{
printf("超时了\n");
}
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (FD_ISSET(0, &myset))
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
if (FD_ISSET(fd, &myset))
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
return 0;
}
4.2 poll机制
实际上poll机制,与select差不多,只是具体调用的实现不一样,与select 不同,poll不是为每个条件构造一个描述符集,而是构造一个 pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
(1)函数原型和所需头文件
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask);
(2)函数功能
- 这两个函数都是为了实现多路复用,这两个函数都能够被信号中断,但是ppoll函数能够通过sigmask参数屏蔽掉那些我们不希望中断ppoll系统调用的信号。
(3)函数参数
a)poll函数
- struct pollfd *fds:struct pollfd结构体数组,每个数组成员设置了我们需要多路IO操作的每个描述符,该结构体成员结构如下:
struct pollfd
{
int fd; /* file descriptor:文件描述符 */
short events; /* requested events:设置我们希望发生的事件 */
short revents; /* returned events :实际发生的事件*/
};
- nfds_t nfds:结构体数组struct pollfd *fds的成员数量。
- int timeout:超时时间,单位为毫秒,如填写为3000,表示3秒超时,如果不希望设置超时,该参数填写负数(如-1)即可。
b)ppoll函数
- 前两个参数同poll函数。
- const struct timespec *timeout:同pselect的超时设置的结构体。
- 如果填写NULL, 在没有描述符响应时和信号中断时则永远阻塞。
- 如果填写了设置超时的结构体地址,在没有描述符响应时则在超时后立即返回。
- sigset_t *sigmask:设置我们希望屏蔽的信号的,防止这些信号中断pselect的调用。
(4)函数返回值
- 返回-1:说明函数调失败,errno被设置。
- 返回0:超时时间到并且没有文件描述符有响应。
- 返回值>0:返回有响应的文件描述符的数量。
(5)注意
- 当描述符有响应时,revents会被内核填写响应的类型,如果events==revents,说明这个响应是我们希望的响应,利用该文件描述符实现相应操作,否则就不是我们希望的响应,不做任何操作。
- 当ppoll被执行时,信号屏蔽字会被sigmask替换,相应的信号会被屏蔽,但是这个函数一旦执行完毕,信号屏蔽字又会被还原为原来的信号屏蔽字,这一点与我们前面学过的pselect是相同的。
- poll和ppoll都是会被信号要中断的低速系统调用,但我们手动重启。
- 如果我们不希望poll、ppoll函数被信号中断,方法同select和pselect。
- 同select、pselect一样,对普通文件进行多路复用是没有意义的。
- poll可模拟精度为ms级别的定时器,而ppoll则也可用来模拟精度为ns级定时的器。
- poll的events和revents标志的设置如下表:
(6)测试用例
poll函数 多路复用读键盘和鼠标,同样打开鼠标需要超级用户权限
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
int main(void)
{
// 读取鼠标
int fd = -1, ret = -1;
char buf[200];
struct pollfd myfds[2] = { 0 };
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 初始化我们的pollfd
myfds[0].fd = 0; // 键盘
myfds[0].events = POLLIN; // 等待读操作
myfds[1].fd = fd; // 鼠标
myfds[1].events = POLLIN; // 等待读操作
ret = poll(myfds, fd + 1, 10000);
if (ret < 0)
{
perror("poll: ");
return -1;
}
else if (ret == 0)
{
printf("超时了\n");
}
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (myfds[0].events == myfds[0].revents)
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
if (myfds[1].events == myfds[1].revents)
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
}
return 0;
}
5 异步IO
所谓异步io就是,当某个事件准备好,进程会被发送一个SIGIO的异步信号,进程受到这个信号的通知后,会调用信号处理函数去处理事件,在事件没有准备好时,进程并不需要轮询事件或者阻塞等待事件,进程可以忙自己的事情直到等到别人发送异步信号SIGIO通知某事件发生。
所谓异步就是,进程接收异步信号的时机完全是随机的,这个时机完全取决于事件发生的时刻,接受信号的进程是没有办法预测的。几乎可以认为异步io就是操作系统用软件实现的一套中断响应系统。
异步IO设置的步骤如下:
- 调用signal或sigaction为该信号建立一个信号处理程序。
- 以命令F_SETOWN调用fcntl来设置接收信号进程PID和进程组GID。
- 以命令F_SETFL调用fcntl设置O_ASYNC状态标志,使在该描述符上可以进行异步I/O。第3步仅用于指向终端或网络的描述符。
5.1 异步IO使用例子
异步IO实现同时读键盘和鼠标
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int mousefd = -1;
// 绑定到SIGIO信号,在函数内处理异步通知事件
void func(int sig)
{
char buf[200] = {0};
if (sig != SIGIO)
return;
read(mousefd, buf, 50);
printf("鼠标读出的内容是:[%s].\n", buf);
}
int main(void)
{
// 读取鼠标
char buf[200];
int flag = -1;
mousefd = open("/dev/input/mouse0", O_RDONLY);
if (mousefd < 0)
{
perror("open:");
return -1;
}
// 注册异步通知
// 把鼠标的文件描述符设置为可以接受异步IO
flag = fcntl(mousefd, F_GETFL);
flag |= O_ASYNC;
fcntl(mousefd, F_SETFL, flag);
fcntl(mousefd, F_SETOWN, getpid()); // 把异步IO事件的接收进程设置为当前进程
// 注册当前进程的SIGIO信号捕获函数
signal(SIGIO, func);
// 读键盘
while (1)
{
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.\n");
read(0, buf, 5);
printf("键盘读出的内容是:[%s].\n", buf);
}
return 0;
}
6 存储映射IO
6.1 存储映射的好处
我们以前为了实现对文件的数据输入/输出,我们用的都是read,write等函数,这些函数处理数据时,数据需要在和各级缓冲区之间进行数据的复制,当要面对大量数据读写时,这些函数调用很费时间,如果我们能够直接通过地址对文件进行数据输入输出的话,将避免这样的缺点,本节的存储映射I/O就能实现这样的功能。
存储映射I/O使一个磁盘文件与虚拟存储空间中的一段缓存相映射。于是当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。这样,就可以在不使用read和write的情况下执行I/O。
6.2 mmap函数
1)函数原型和所需头文件
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
2)函数功能
- mmap函数:将文件所在的磁盘空间映射到内存空间。
- munmap函数:取消映射。
3)函数参数
a)void *addr:在内存中映射时的映射起始地址。
- 如果设置为NULL,由内核决定起始地址,这时最常见的方式。
- 如果设置不为NULL,内核将采用该地址作为映射起始地址,如果这个地址不是内 存页的整数倍,内核会自动调整到最近的虚拟页整数倍的地址处。
b)size_t length:映射的长度。
c)int prot:指定对映射区的操作权限,可指定如下命令宏:
- PROT_EXEC: 映射区的内容可执行。
- PROT_READ: 映射区的内容可读。
- PROT_WRITE: 映射区的内容可写。
- PROT_NONE: 映射区不允许访问。
前三个选项可相互|操作,如果已经设置了前三个中一个或几个参数的话,设置第四个 参数就没有意义了,如果你想设置PROT_NONE,就不要设置前三个PROT_XXX参数。
d)int flags:该标志决定对映射区的操作是否同步地更新文件内容,该映射区对于其它也映射了文件该区域的进程是否可见,这些行为都有下述标志决定:
- MAP_SHARED:共享映射区。指定该标志后本进程对于映射区的修改将会被更新到文件中,所以对于其它也映射了该区的进程来说文件的修改是可见的。
- MAP_PRIVATE:创建私有的写时复制映射区。指定了此标志后,对于映射区的修改是不会去更新文件的,所以对于其它也映射了该文件的其它进程来说是不可见的。
- MAP_FIXED:返回值必须等于addr。由于该设置不利于可移植性,所以不鼓励使用此标志,如果未指定此标志,但addr却非0,那么内核只把addr视为将何处设置为映射起始地址的一种建议。
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
int main(void)
{
int fd = open("./test.txt", O_RDWR);
char *p;
int i;
struct stat sbuf;
stat("./test.txt", &sbuf);
int len = sbuf.st_size;
printf("len = %d\n", len);
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
strcpy(p, "hehehe");
for (i = 0; i < len; i++)
{
printf("%c", p[i]);
}
printf("\n");
munmap(p, len);
close(fd);
return 0;
}
运行结果:linux@ubuntu64-vm:~/workdir/test$ cat test.txt heheheorld!
e)int fd:指向需要被映射文件的描述符。
f)off_t offset:指定从文件起始位置偏移多少字节处开始映射。
4)函数返回值
- mmap函数:函数调用成功,返回分配的映射地址,失败则返回MAP_FAILED(void *)-1,errno被设置。
- munmap函数:调用成功返回 0, 失败则-1, errno被设置。
5)注意
a)设置addr和offset的值时,其值通常应该是虚拟页的整数倍,虚存页长度可用带参数_SC_PAGESIZE的sysconf函数得到。因为offset和addr常常指定为0,所以这种要求一般并不是问题。
b)映射时遵守如下规则:
需要被映射文件的长度不能为0,如果文件长度为0,则可以向文件写入一点数据或者调用ftruncate函数对文件设置一个初始长度。映射时,不管length指定的大小是多少(length不能==0),真实映射的空间大小如下:
if(文件长度 % 虚拟页 != 0) 真实映射空间 = 虚拟页*(文件大小/虚拟页 +1) n*4KB
else if(文件长度 % 虚拟页 == 0) 真实映射空间 = 文件大小
根据mmap时指定的映射长度length和真实映射空间的大小关系,映射空间的情况分为如下几种情况:
情况一:length > 真实映射空间,映射情况如下:
- 写有效映射空间:如果我们mmap时指定了PROT_SHARED标志的话,写入内存中内容会同步更新到文件中。
- 写虚空间:写操作是有效的,但是只是写到了内存中,文件并不会被更新,因为文件长度不包含这部分。
- 写无效映射空间:会导致SIGBUS信号的产生,对这个信号默认处理方式会终止进 程。实际上导致无效映射空间产生的有两种。
- mmap时指定的length > 真实映射空间。
- ftruncate将文件截断,真实映射的自动空间缩短,导致指定的length > 缩短后的真实映射空间。
- 当文件长度为0时,我们映射的空间都是无效映射空间。
情况二:length < 真实映射空间(length<文件长度就更不用说了),映射情况如下:
- 写有效映射空间:如果我们mmap时指定了PROT_SHARED标志的话,写入内存中的内容会被更新到文件中。
- 写虚空间:写操作是有效的,但是只是写到了内存中,文件不会被更新的,因为文件长度不包含这部分。
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(void)
{
char *mem;
//int fd = open("hello244", O_RDWR|O_CREAT|O_TRUNC, 0644);
int fd = open("./dict.txt", O_RDWR | O_TRUNC);
if (fd < 0)
sys_err("open error");
ftruncate(fd, 20);
mem = mmap(NULL, 20, PROT_WRITE | PROT_WRITE, MAP_SHARED, fd, 0);
if (mem == MAP_FAILED) //出错判断
sys_err("mmap err: ");
close(fd); // 文件描述符先关闭,对mmap映射没有影响
strcpy(mem, "dKASjfVCGBK.jSGAFkjagsk>gFK"); // 大于20字节,可以写入映射去,但是只同步20字节到文件
printf("%s\n", mem);
//mem++; // munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
if (munmap(mem, 20) < 0)
sys_err("munmap");
return 0;
}
/*
1. 可以open的时候O_CREAT一个新文件来创建映射区吗 ?可以
2. 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ | PROT_WRITE会怎样?出错,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3. 文件描述符先关闭,对mmap映射有没有影响?没有
4. 如果文件偏移量为1000会怎样?报错,文件偏移量必须为4K的整数倍
5. 对mem越界操作会怎样?写虚映射区,只是写到了内存中,文件并不会被更新
6. 如果mem++,munmap可否成功?
7. mmap什么情况下会调用失败?munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
8. 如果不检测mmap的返回值,会怎样?
*/
c)与存储映射有关的有两个信号,SIGBUS和SIGSEGV。
第一个信号产生的原因我们已经清楚,但是产生SIGSEGV信号的原因有哪些呢?
- 访问的空间并不存在
- 访问文件的权限不满足open时或者mmap是指定的权限。
d)open是指定的权限与mmap时指定的权限之间的关系
- open文件时权限一定要包含读权限,否者mmap会因权限不足而调用失败,比如如果open文件时只指定O_WRONLY权限,mmap因权限受限而错误返回。
- mmap时指定的权限必须是open文件时所允许的权限,如果open文件时没有指定写权限,即使mmap时指定了写权限,mmap时会因权限受限而错误返回。但是如果open时指定了读权限,但是mmap时却没有指定读权限,对于这种情况确是允许读映射空间的,换句话说相当于mmap时会默认指定读权限。
mmap父子进程通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
- MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
- MAP_SHARED: (共享映射) 父子进程共享映射区;
练习:父进程创建映射区,然后fork子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("./temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
unlink("./temp"); //删除临时文件目录项,使之具备被释放条件.
ftruncate(fd, 4);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED)
{ //注意:不是p == NULL
perror("mmap error");
exit(-1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork();//创建子进程
if (pid < 0)
{
perror("fork error");
exit(-1);
}
else if(pid == 0)
{
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
}
else
{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
int ret = munmap(p, 4); // 释放映射区
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
}
return 0;
}
运行结果
结论:父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)。
匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(void)
{
int *p;
pid_t pid;
p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); //MAP_ANONYMOUS
if(p == MAP_FAILED) //注意:不是p == NULL
{
perror("mmap error");
exit(-1);
}
pid = fork();//创建子进程
if (pid < 0)
{
perror("fork error");
exit(-1);
}
else if(pid == 0)
{
*p = 2000;
printf("child, *p = %d\n", *p);
}
else
{
sleep(1);
printf("parent, *p = %d\n", *p);
wait(NULL);
}
int ret = munmap(p, 4); // 释放映射区
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
return 0;
}
运行结果
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
- fd = open("/dev/zero", O_RDWR); ② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
- /dev/zero 聚宝盆,可以随意映射;/dev/null 无底洞,一般错误信息会重定向到这个文件中。
mmap无血缘关系进程间通信
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。
(文件是mmap无血缘关系进程间通信的媒介,匿名映射不能实现无血缘关系进程间通信)
mmap_w.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU
{
int id;
char name[20];
char sex;
};
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student = {1, "xiaoming", 'm'};
struct STU *mm;
if (argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0664);
ftruncate(fd, sizeof(student));
mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap");
close(fd);
while (1)
{
memcpy(mm, &student, sizeof(student)); // 没隔一秒修改映射区内容
student.id++;
sleep(1);
}
int ret = munmap(mm, sizeof(student)); // 释放映射区
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
return 0;
}
mmap_r.c
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU
{
int id;
char name[20];
char sex;
};
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;
if (argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDONLY);
if (fd == -1)
sys_err("open error");
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap error");
close(fd);
while (1)
{
printf("id = %d\tname=%s\t%c\n", mm->id, mm->name, mm->sex);
sleep(1);
}
int ret = munmap(mm, sizeof(student)); // 释放映射区
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
return 0;
}
运行结果