Linux C 进程间通信基本操作
虚拟CPU和虚拟内存的引入保证了进程的一个重要特性就是隔离,一个进程在执行过程中总是认为自己占用了所有的CPU和内存,但是实际在底层,操作系统和硬件完成了很多工作才实现了隔离的特性(比如内核和时钟设备配合实现进程调度)。在多个进程之间,如果需要进行通信的话,隔离特性会造成一些通信的障碍。所以我们需要一些手段来跨越隔离,实现进程间通信(InterProcess Communication,IPC)。
管道
多进程之间一种最自然的通信方式就是依赖文件系统,一个进程打开文件并读写信息,另一个进程也可以打开文件来获取信息。显然,这种通信方式要依赖磁盘文件,效率十分低下。为了提升效率,有名管道(named pipe,FIFO)就被设计出来了,有名管道是文件系统中一种专门用来读写的文件,但是通过有名管道进行通信的时候实际上并没有经过磁盘,而是经过内核的管道缓冲区进行数据传递。
如果对于拥有亲缘关系的进程而言,它们之间可以使用另一种匿名管道。匿名管道又可以直接被称为管道,它不需要在文件系统创建单独的文件,相反它是进程在执行过程中动态创建和销毁的。这种管道只能在父子进程间使用。
由于之前已经对管道文件进行过总结,这里不再赘述:Linux C 管道文件操作
共享内存
在进程的数量比较少的时候,使用管道进行通信是比较自然的思路。如果需要在任意两个进程之间使用管道,它需要调用两次 pipe 系统调用,但是随着进程数量增加, pipe 的使用次数将会急剧增加。除此以外,使用管道通信的时候,数据要从写端拷贝到内核管道缓冲区,在从缓冲区拷贝到读端,总共要进行两次拷贝,性能比较差。为了提升进程间通信的效率,共享内存(也有翻译成共享存储)的方式就诞生了。
对于进程而言,代码中所使用的地址都是虚拟地址,所操作的地址空间是虚拟地址空间。当进程执行的时候,如果发生访问内存的操作,操作系统和硬件需要能保证虚拟地址能够映射到物理内存上面,和这种地址转换相关的设备被称为内存管理单元(MMU)。因此,如果两个不同的进程使用相同的虚拟地址,它们所对应的物理内存地址是不一样的。共享内存就允许两个或者多个进程共享一个给定的物理存储区域。当然为了实现共享,内核会专门维护一个用来存储共享内存信息的数据结构,这用不同的进程就可以通过共享内存进行通信了。
共享内存的思想实际上是非常普遍的。对于使用C语言书写的程序,使用C标准库是非常频繁的。对于每一个使用C标准库的进程,如果都去打开磁盘中的动态库文件,那么就会消耗巨大的存储资源去存储重复数据。所以使用共享内存的方法在内存中常驻这些经常使用的文件的思路是非常自然的。
ftok
ftok
是一个用于生成共享内存、消息队列或信号量的键值(key_t
)的函数。它通过给定的路径名和项目标识符生成一个唯一的键值,通常用于 POSIX 系统(如 Linux 和 Unix)中的进程间通信(IPC)。
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
参数说明
1. pathname
-
类型:
const char *
-
含义:指向一个以空字符结尾的字符串,表示一个存在的文件路径名。这个路径名用于生成键值,但它本身不会被打开或修改。
-
要求:文件路径必须存在,否则
ftok
会失败。路径名通常是一个临时文件或已存在的文件,用于确保生成的键值具有唯一性。
2. proj_id
-
类型:
int
-
含义:项目标识符,通常是一个字符(如
'a'
、'b'
等)。它用于进一步区分同一路径名下的不同键值。 -
要求:
proj_id
的值必须在 1 到 255 之间(即0x01
到0xFF
)。超出这个范围的值可能会导致未定义行为。
返回值
-
成功:返回一个
key_t
类型的键值,该键值可以用于shmget
、msgget
或semget
等 IPC 系统调用。 -
失败:返回
(key_t)-1
,并设置errno
以指示错误原因。
ftok
的主要功能是生成一个唯一的键值,用于标识共享内存段、消息队列或信号量。它通过以下步骤生成键值:
-
使用路径名的 inode 号(文件的唯一标识符)。
-
结合项目标识符(
proj_id
)。 -
生成一个唯一的
key_t
值。
int main(int argc, char *argv[])
{ARGS_CHECK(argc,2);key_t key = ftok(argv[1],1);ERROR_CHECK(key,-1,"ftok");printf("key = %d\n", key);return 0;
}
共享内存、信号量和消息队列一旦创建以后,即使进程已经终止,这些IPC并不会释放在内核中的数据结构,可以用使用命令 ipcs 来查看这些IPC的信息。
(base) ubuntu@ubuntu:~$ ipcs--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息 ------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x5104002d 262146 ubuntu 600 4096 4
0x51040031 262147 ubuntu 600 1 1
0x51040033 262148 ubuntu 600 1 4
0x00000000 6 ubuntu 600 16384 1 目标
0x00000000 229383 ubuntu 600 16384 1 目标
0x51040037 262152 ubuntu 600 524296 3
0x00000000 229385 ubuntu 600 8077312 2 目标
0x5104003b 262154 ubuntu 600 1 1
0x5104003d 262155 ubuntu 600 1 1
0x00000000 12 ubuntu 777 4000 2 目标$ipcs -l
# 查看各个IPC的限制
$ipcrm -m shmid
# 手动删除
shmget
使用 shmget 接口可以根据键来获取一个共享内存段。无论是创建新共享内存段,还是引用现存的内存段,都可以使用 shmget 函数。创建的共享内存段的所有字节会被初始化为0。key参数表示传入的键,键的取值可以是一个正数或者是宏 IPC_PRIVATE 。size表示共享内存的大小,其取值应当是页大小的整数倍。shmflg表示共享内存的属性,其最低9位表示各个用户对其的读/写/执行权限(当然IPC的执行权限事实上是无用的)。 shmget 的返回值表示共享内存段的描述符,以供后续使用。
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
参数说明
1. key
-
类型:
key_t
-
含义:共享内存段的唯一标识符(键值)。
key
用于区分不同的共享内存段。 -
来源:
-
IPC_PRIVATE
:如果设置为IPC_PRIVATE
,则创建一个匿名共享内存段,只有创建它的进程及其子进程可以访问。 -
ftok
生成的键值:通过ftok
函数生成的键值,通常用于命名共享内存段,允许多个进程通过相同的键值访问同一个共享内存段。
-
2. size
-
类型:
size_t
-
含义:共享内存段的大小(以字节为单位)。此参数仅在创建新的共享内存段时生效。
-
要求:必须是一个正整数,表示共享内存段的大小。如果
key
指向一个已存在的共享内存段,则size
参数会被忽略。
3. shmflg
-
类型:
int
-
含义:标志位,用于指定共享内存段的访问权限和其他选项。
-
常用标志:
-
权限标志:使用八进制表示,如
0666
(所有用户可读写)、0644
(所有者可读写,同组用户可读)。 -
创建标志:
-
IPC_CREAT
:如果指定的共享内存段不存在,则创建一个新的共享内存段。 -
IPC_EXCL
:与IPC_CREAT
一起使用,如果共享内存段已经存在,则返回错误。
-
-
返回值
-
成功:返回共享内存段的标识符(非负整数)。这个标识符可以用于后续的共享内存操作,如
shmat
(附加共享内存段)和shmctl
(控制共享内存段)。 -
失败:返回
-1
,并设置errno
以指示错误原因。
创建示例:
#include<54func.h>
int main(int argc, char *argv[])
{ARGS_CHECK(argc,2);key_t key = ftok(argv[1],1);ERROR_CHECK(key,-1,"ftok");printf("key = %d\n", key);int shmid = shmget(key,4096,0600|IPC_CREAT);ERROR_CHECK(shmid,-1,"shmget"); printf("shmid = %d\n", shmid);return 0;
}
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/process$ ./ftok /home/ubuntu
key = 17039362
shmid = 426145
(base) ubuntu@ubuntu:~$ ipcs | grep 426145
0x01040002 426145 ubuntu 600 4096 0
(base) ubuntu@ubuntu:~$ ipcrm -m 426145
shmat
shmat
的主要功能是将共享内存段附加到调用进程的地址空间中。附加后,共享内存段就像进程的普通内存一样,可以通过指针进行读写操作。具体行为取决于 shmaddr
和 shmflg
参数
#include <sys/ipc.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明
1. shmid
-
类型:
int
-
含义:共享内存段的标识符,由
shmget
函数返回。
2. shmaddr
-
类型:
const void *
-
含义:指定共享内存段附加到进程地址空间的地址。通常设置为
NULL
,让系统自动选择地址。 -
选项:
-
NULL
:让系统自动选择一个合适的地址。 -
指定地址:如果需要将共享内存段附加到特定的地址,可以提供一个非
NULL
的地址。但这种方式需要谨慎使用,因为如果指定的地址已经被占用或不可用,会导致附加失败。
-
3. shmflg
-
类型:
int
-
含义:标志位,用于指定附加操作的选项。
-
常用标志:
-
0
:默认行为,以读写方式附加共享内存段。 -
SHM_RDONLY
:以只读方式附加共享内存段。如果设置了此标志,进程只能读取共享内存段中的数据,不能写入。
-
返回值
-
成功:返回共享内存段的起始地址(指向共享内存段的指针)。进程可以通过这个指针访问共享内存段中的数据。
-
失败:返回
(void *)-1
,并设置errno
以指示错误原因。
示例:
int main()
{int shmid = shmget(1000,4096,0600|IPC_CREAT);//如果key为1000的共享内存已经存在,则找到它的描述符ERROR_CHECK(shmid,-1,"shmget");char *p = (char *)shmat(shmid,NULL,0);ERROR_CHECK(p,(char *)-1,"shmat");while(1);return 0;
}//当进程正在运行的时候,nattch为1,终止进程以后,nattch为0
shmdt
shmdt
用于将共享内存段从调用进程的地址空间中分离。分离后,进程将无法再通过之前附加的地址访问共享内存段。分离操作不会删除共享内存段,只是将其从当前进程的地址空间中移除。
#include <sys/ipc.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);
参数说明
1. shmaddr
-
类型:
const void *
-
含义:指向共享内存段的起始地址,该地址是之前通过
shmat
附加到进程地址空间的。 -
要求:
-
必须是
shmat
返回的有效地址。 -
如果传递的地址无效或未附加到任何共享内存段,调用将失败。
-
-
示例
char *shmaddr = (char *)shmat(shmid, NULL, 0);
返回值
-
成功:返回
0
。 -
失败:返回
-1
,并设置errno
以指示错误原因。
示例:
int main(){int shmid = shmget(1000,4096,0600|IPC_CREAT);//key为1000 大小为4096 创建一个0600的共享内存ERROR_CHECK(shmid,-1,"shmget");char *p = (char *)shmat(shmid,NULL,0);ERROR_CHECK(p,(char *)-1,"shmat");int ret = shmdt(p);ERROR_CHECK(ret,-1,"shmdt");while(1);return 0;
}
使用共享内存进行进程间通信
共享内存可以在两个互不关联的进程之间进行通信,只需要彼此之间知道共享内存的键就好了。
//shm_r.c
int main(int argc, char const *argv[]){ARGS_CHECK(argc, 2);key_t key = ftok(argv[1], 1);ERROR_CHECK(key, -1, "ftok");int shmid = shmget(key, 4096, IPC_CREAT | 0666);ERROR_CHECK(shmid, -1, "shmget");printf("shmid = %d\n", shmid);char *p = (char *)shmat(shmid, NULL, 0);ERROR_CHECK(p, (char *)-1, "shmat");strcpy(p, "I love you");return 0;
}
//shm_w.c
int main(int argc, char const *argv[]){ARGS_CHECK(argc, 2);key_t key = ftok(argv[1], 1);ERROR_CHECK(key, -1, "ftok");int shmid = shmget(key, 4096, IPC_CREAT | 0666);ERROR_CHECK(shmid, -1, "shmget");printf("shmid = %d\n", shmid);char *p = (char *)shmat(shmid, NULL, 0);ERROR_CHECK(p, (char *)-1, "shmat");printf("%s\n",p);return 0;
}