进程间通信1, 管道1.1 有名管道1.2 匿名管道1.3 popen2, 共享内存2.1 System V2.1.1 生产标识2.1.2 创建共享内存2.1.3 获取共享内存2.1.4 共享内存通信2.1.5 解除共享内存映射2.1.6 修改共享内存属性: 仅了解2.2 POSIX2.3 同时写入
虚拟CPU和虚拟内存的引入保证了进程的一个重要特性就是隔离,一个进程在执行过程中逻辑上总是认为自己占用了所有的CPU和内存,但是实际在底层,操作系统和硬件完成了很多工作才实现了隔离的特性(比如内核和时钟设备配合实现进程调度)。在多个进程之间,如果需要进行通信的话,隔离特性会造成一些通信的障碍。所以我们需要一些手段来跨越隔离,实现进程间通信(InterProcess Communication,IPC)。
一种最自然的通信方式就是依赖文件系统, 一个进程打开文件并读写信息,另一个进程也可以打开文件来获取信息。显然,这种通信方式要依赖磁盘文件,效率十分低下。
为了提升效率, 有名管道(named pipe,FIFO)就被设计出来了,有名管道是文件系统中一种专门用来读写的文件,但是通过有名管道进行通信的时候实际上并没有经过磁盘,而是经过内核的管道缓冲区进行数据传递。
如果对于拥有亲缘关系的进程而言,它们之间可以使用另一种匿名管道。匿名管道它不需要在文件系统创建单独的文件,相反它是进程在执行过程中动态创建和销毁的。
有名管道在文件系统中以文件的形式存在,但它允许进程之间以先进先出的方式进行通信。它可以使用mkfifo可以创建管道文件,使用unlink删除管道文件。(man 3 mkfifo)
xxxxxxxxxx
8123// make a FIFO special file (a named pipe)
4int mkfifo(
5const char *pathname, // 要创建的命名管道的路径名
6mode_t mode // 指定新管道的权限(掩码)
7);
8// 返回值: 成功0, 失败-1
xxxxxxxxxx
612// delete a name and possibly the file it refers to
3int unlink(
4const char *pathname // 要删除的文件的路径名
5);
6// 返回值: 成功0, 失败-1
EgCode:
xxxxxxxxxx
231int main(int argc,char*argv[])
2{
3mkfifo("myfifo", 0600);
4
5int fd = open("myfifo", O_RDWR);//可读可写
6if(fork() == 0){
7write(fd, "hello", 5);
8sleep(1);
9
10char buf[5] = {0};
11read(fd, buf, sizeof(buf));
12printf("child: %s \n", buf);
13}else{
14char buf[5] = {0};
15read(fd, buf, sizeof(buf));
16printf("main: %s \n", buf);
17
18sleep(1);
19write(fd, "ok", 2);
20}
21unlink("myfifo");
22return 0;
23}
匿名管道/pipe函数是Linux系统中用于实现父子进程间通信的一种简单方式。 (man pipe)
管道是单向的,数据从一端流入,从另一端流出,因此它们是半双工的。(这就意味着: 使用匿名管道,一个进程将数据写入管道pipe[1],而另一个进程则可以从管道读取数据pipe[0])
匿名管道只使用于存在亲缘关系的进程之间进行通信(Eg: 父子进程)。
先在一个进程中创建一个管道,然后再利用fork, 可以让父子进程同时持有管道, 就可以实现进程间通信了。(单个进程的管道的是没有什么价值的)
如果要实现父子进程之间全双工通信,需要调用pipe函数两次来创建两条管道。而且, fork的次数和管道的数量无关,每一次使用 pipe函数就会在内核创建一个管道缓冲区。
xxxxxxxxxx
612// create pipe
3int pipe(
4int pipefd[2]//包含两个文件描述符的整型数组。pipefd[0]读取端文件描述符,pipefd[1]写端文件描述符。
5);
6// 返回值: 成功0, 失败-1
EgCode:
xxxxxxxxxx
181int main(int argc,char*argv[])
2{
3int pipefd[2];
4pipe(pipefd);
5
6if(fork() == 0){
7close(pipefd[1]); // 只保留读
8char buf[5];
9read(pipefd[0], buf, 5);
10
11printf("child get message: %s \n", buf);
12}else{
13close(pipefd[0]); // 只保留写
14write(pipefd[1], "hello", 5);
15wait(NULL);
16}
17return 0;
18}
popen()函数是一个在 Linux系统中常用的库函数,它用于创建一个管道,并且同时启动一个新的进程,然后将这个启动的新进程的标准输入或标准输出与管道连接起来。(man 3 popen)
这允许当前程序和新启动的程序之间进行数据交换。
popen()
函数的本质是对pipe()
,fork()
, 等函数的高级封装。xxxxxxxxxx
812// pipe stream to or from a process
3FILE *popen(
4const char *command, // 要执行的命令
5const char *type // 通信的方向: r表示当前程序可以从新启动的程序的标准输出中读取数据。
6// w表示当前程序可以向新启动的程序的标准输入写入数据。
7);
8// 返回值: 成功返回一个FILE指针,通过这个指针,可以使用标准的输入/输出库函数来读取或写入数据[如 fread、fwrite、fgets、fputs等]。失败返回 NULL。
EgCode1:
xxxxxxxxxx
111int main(int argc,char*argv[])
2{
3char buf[1024];
4FILE *pipe = popen("ls", "r");//读取ll命令的结果
5
6fread(buf,1, sizeof(buf), pipe);
7
8printf("ls: \n%s", buf);
9pclose(pipe);
10return 0;
11}
EgCode2:
xxxxxxxxxx
61// 04_printf_str
2int main(int argc,char*argv[])
3{
4printf("hello \n");
5return 0;
6}
xxxxxxxxxx
111int main(int argc,char*argv[])
2{
3char buf[1024];
4FILE *pipe = popen("./04_printf_str", "r");//读取04_printf_str的标准输出
5
6fread(buf, 1, sizeof(buf), pipe);
7
8printf("04_printf_str: \n%s", buf);
9pclose(pipe);
10return 0;
11}
EgCode3:
xxxxxxxxxx
91// 05_sum
2int main(int argc,char*argv[])
3{
4int num1;
5int num2;
6scanf("%d%d",&num1, &num2);
7printf("sum: %d \n", num1+num2);
8return 0;
9}
xxxxxxxxxx
91int main(int argc,char*argv[])
2{
3FILE *pipe = popen("./05_sum", "w");//向05_sum的标准输入写数据
4
5fwrite("3 4", 1, 3, pipe);
6
7pclose(pipe);
8return 0;
9}
在进程的数量比较少的时候,使用管道进行通信是比较自然的思路。如果需要在任意两个进程之间使用匿名管道,它需要调用两次 pipe系统调用,但是随着进程数量增加, pipe的使用次数将会急剧增加。除此以外,使用管道通信的时候,数据要从写端拷贝到内核管道缓冲区, 再从缓冲区拷贝到读端,总共要进行两次拷贝,性能比较差。为了提升进程间通信的效率, 共享内存 (也有翻译成共享存储)的方式就诞生了。
对于进程而言,代码中所使用的地址都是虚拟地址,所操作的地址空间是虚拟地址空间。当进程执行的时候,如果发生访问内存的操作,操作系统和硬件需要能保证虚拟地址能够映射到物理内存上面( 这种地址转换相关的设备被称为内存管理单元MMU) (所以如果两个不同的进程即使使用相同的虚拟地址,它们所对应的物理内存地址是不一样的)。
而共享内存就允许两个或者多个进程共享一个给定的物理存储区域 ,这使得不同的进程就可以通过共享内存进行通信了。(当然为了实现内存共享,内核会专门维护一个用来存储共享内存信息的数据结构 : 其中包括共享内存大小、权限、引用进程数量....)。
对于Linux的共享内存, 有两个常见的版本
System V
和POSIX接口
。
ftok函数的作用是根据条件产生一个独特的、用于标识作用的键值,使得在SystemV共享内存机制中, 不同的进程可以通过同一键值访问到同一片共享内存,从而实现进程间通信。(man 2 ftok)
xxxxxxxxxx
8123// convert a pathname and a project identifier to a System V IPC key
4key_t ftok(
5const char *pathname, // 实际存在且可访问的文件的路径(文件本身内容并不重要, 只是标识作用)
6int proj_id // 进一步确保生成的键值的唯一性(也是标识作用), 1-255(不能是0)
7);
8// 返回值: 成功则返回产生键值, 失败返回-1
使用shmget函数可以根据键来获取一个共享内存段。
函数的键值key通常由ftok函数生成。
如果参数key设置为IPC_PRIVATE的宏,函数总会创建一个新的共享内存段,且只有创建这个共享内存的进程,及其具有亲缘关系的进程(子进程)可以访问。 (设置IPC_PRIVATE创建的共享内存又被称为私有内存)
建议size为内存页大小的整数倍。
shmflg参数, 可以设置共享内存的读写权限。 (Eg: 0600 ...)
shmflg参数, 还可以配合逻辑标准使用。( IPC_CREAT、IPC_EXCL )
xxxxxxxxxx
20123//allocates a System V shared memory segment
4int shmget(
5key_t key, // 共享内存段的键值, 用于标识共享内存段
6size_t size, // 指定共享内存段的大小(单位:字节)
7int shmflg // 位掩码,用于设置共享内存段的权限和状态
8);
9// 返回值:
10// 情况一: 如果shmflg, 既没有设置IPC_CREAT, 也没有设置IPC_EXCL, 只有文件权限
11如果指定的共享内存(key来确定)已经存在:返回已存在的共享内存段的标识符
12如果指定的共享内存(key来确定)不存在: 将失败,并返回 -1
13// 情况二: 如果shmflg, 设置了文件权限之后, 也设置了IPC_CREAT
14如果指定的共享内存(key来确定)已经存在:返回已存在的共享内存段的标识符
15如果指定的共享内存(key来确定)不存在: 会创建一个新的共享内存段,并返回该共享内存标识符
16// 情况三: 如果shmflg, 设置了文件权限之后, 也设置了IPC_CREAT, 也设置了IPC_EXCL
17如果指定的共享内存(key来确定)已经存在:会失败返回-1,errno被设置为 EEXIST
18如果指定的共享内存(key来确定)不存在: 会创建一个新的共享内存段,并返回该共享内存标识符
19
20ps: IPC_EXCL标志单独使用无意义(等同第一种情况), IPC_EXCL标志只有在与IPC_CREAT一起使用时才有意义。
EgCode:
xxxxxxxxxx
111int main(int argc,char*argv[])
2{
3key_t keytag = ftok("./Makefile", 1); // 获得key标记
4printf("key_t: %d \n", keytag);
5
6int shmid = shmget(keytag, 100, 0600|IPC_CREAT);// 产生共享内存
7ERROR_CHECK(shmid, -1, "shmget");
8printf("shmid: %d \n", shmid);
9
10return 0;
11}
查看共享内存命令
如共享内存、以及信号量和消息队列一旦创建以后,即使进程已经终止,这些IPC并不会释放在内核中的数据结构,可以用使用命令ipcs来查看这些IPC的信息。
xxxxxxxxxx
91$ipcs
2# key shmid owner perms bytes nattch status
3# 键 描述符 拥有者 权限 占据空间 连接数 状态
4
5$ipcs -l
6# 查看各个IPC的限制
7
8$ipcrm -m shmid
9# 手动删除
shmat函数用于将共享内存段映射到当前进程的地址空间,允许进程访问共享内存段中的数据。(man 2 shmat)
xxxxxxxxxx
9123//memory operations
4void *shmat(
5int shmid, // 共享内存标识符,由 shmget 返回。
6const void *shmaddr, // 指定共享内存连接到进程地址空间的具体地址。建议NULL自动选择
7int shmflg // 标识权限。意义不大填0即可
8);
9// 返回值: 成功, 返回共享内存段在进程地址空间中的起始地址, 这个地址是进程用于访问共享内存的指针。失败-1
EgCode: 向共享内存写内容
xxxxxxxxxx
91int main(int argc,char*argv[])
2{
3int shmid = shmget(1000, 4096, 0600|IPC_CREAT); // 简单指定key: 1000
4char *p = (char *)shmat(shmid, NULL, 0);
5
6strcpy(p, "hello123");
7
8return 0;
9}
EgCode: 从共享内存读内容
xxxxxxxxxx
91int main(int argc,char*argv[])
2{
3int shmid = shmget(1000, 4096, 0600|IPC_CREAT); // 简单指定key: 1000
4char *p = (char *)shmat(shmid, NULL, 0);
5
6printf("read: %s \n", p);
7
8return 0;
9}
使用 shmdt函数可以断开当前进程与共享内存段的连接,解除到共享内存段的映射(某种程度上等价于free)。(当一个进程通过 shmat函数将共享内存段附加到自己的地址空间后,使用 shmdt函数将其分离。) (man 2 shmdt)
xxxxxxxxxx
7123//shared memory operations
4int shmdt(
5const void *shmaddr // 指向共享内存段的起始地址(shmat返回值)
6);
7// 返回值: 成功0, 失败-1
EgCode:
xxxxxxxxxx
131int main(int argc,char*argv[])
2{
3int shmid = shmget(1000, 4096, 0600|IPC_CREAT);
4char *p = (char *)shmat(shmid, NULL, 0);
5
6strcpy(p, "hello123");
7shmdt(p);
8
9while(1){
10strcpy(p, "456");
11}
12return 0;
13}
使用shmctl函数可以用于对共享内存段执行多种操作。根据参数的不同,可以执行不同的操作。 (man shmctl)
IPC_STAT可以用来获取存储共享内存段信息的数据结构;
IPC_RMID可以用来从内核删除共享内存段,当删除时,无论此时有多少进程映射到共享内存段,它都会被标记为待删除, 一旦被标记以后,就无法再建立映射了。当最后一个映射解除时,共享内存段就真正被移除。
IPC_SET可以用来修改共享内存段的所有者、所在组和权限;
EgCode:
xxxxxxxxxx
131//获取共享内存段信息
2int main()
3{
4int shmid = shmget(1000,4096,0600|IPC_CREAT);
5char *p = (char *)shmat(shmid,NULL,0);
6
7struct shmid_ds stat;
8int ret = shmctl(shmid,IPC_STAT,&stat);
9
10printf("cuid = %d perm = %o size= %ld nattch = %ld\n",
11stat.shm_perm.cuid,stat.shm_perm.mode,stat.shm_segsz,stat.shm_nattch);
12return 0;
13}
xxxxxxxxxx
81//删除共享内存
2int main()
3{
4int shmid = shmget(1001,4096,0600|IPC_CREAT);
5char *p = (char *)shmat(shmid,NULL,0);
6int ret = shmctl(shmid,IPC_RMID,NULL);
7return 0;
8}
xxxxxxxxxx
141//修改共享内存段的权限
2int main()
3{
4int shmid = shmget(1000,4096,0600|IPC_CREAT);
5char *p = (char *)shmat(shmid,NULL,0);
6struct shmid_ds stat;
7int ret = shmctl(shmid,IPC_STAT,&stat);
8printf("cuid = %d perm = %o size= %ld nattch = %ld\n",
9stat.shm_perm.cuid,stat.shm_perm.mode,stat.shm_segsz,stat.shm_nattch);
10
11stat.shm_perm.mode = 0666;
12ret = shmctl(shmid,IPC_SET,&stat);
13return 0;
14}
EgCode:
xxxxxxxxxx
25123456789int main() {
10int shm_fd = shm_open("/test", O_CREAT | O_RDWR, 0666);
11ftruncate(shm_fd, 4096);
12
13void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
14
15if (fork() == 0) {
16sprintf((char *)ptr, "Hello from child!");
17} else {
18wait(NULL);
19printf("shared memory: %s \n", (char *)ptr);
20}
21
22munmap(ptr, 4096);
23shm_unlink("/test");
24return 0;
25}
在上述例子中: 使用基本和System V一致
使用
shm_open
创建一个新的或打开一个共享内存对象。使用
ftruncate
设置共享内存对象的大小。使用
mmap
将共享内存映射到进程的地址空间。通过
fork
创建一个新的进程。在子进程中,向共享内存写入数据。
在父进程中,等待子进程结束后,从共享内存读取数据。
使用
munmap
解除映射。使用
shm_unlink
删除共享内存对象。
ps1: 上述代码编译需要注意链接 -lrt (参考 man shm_open). (Eg: gcc test.c -o test -lrt)
ps2: shm_open 函数的第一个参数是一个POSIX共享内存对象的名字,而不是文件系统上的一个路径,而是一个标识符,应该以 / 开头的名字,用于创建和引用共享内存对象。 (这和ftok是不同的)
共享内存可以实现多个进程同时对同一个数据进行访问和修改。
xxxxxxxxxx
201int main(int argc,char*argv[])
2{
3int shmid = shmget(100, 4096, 0600|IPC_CREAT);
4int *p = (int *)shmat(shmid, NULL, 0);
5p[0] = 0;
6
7if(fork() == 0){
8for(int i = 0; i < 100000; i++){
9p[0]++;
10}
11}else{
12for(int i = 0; i < 100000; i++){
13p[0]++;
14}
15
16wait(NULL);
17printf("%d \n", p[0]);
18}
19return 0;
20}
执行上述程序,所得到的结果和预期的200000并不一致。进程的切换是造成结果不一致 的原因:当其父进程进程尝试对共享资源进行访问的时候,比如将p[0]从共享内存取出到寄存器当中时,此时父进程进程有可能被切换到子进程进程,而子进程进程会修改p[0]的数值并写回到共享内存中,当重新切换回父进程进程的时候, 父进程进程会继续刚才的指令继续运行,就会将寄存器当中p[0]的内容写回内存中,这样的话子进程进程刚刚的修改就丢失掉了 (反之亦然)。这个多个进程同时写入造成结果出错误的情况被称为竞争条件。
man -k shm