Linux进程间通信
进程间通信方式比较
-
管道:速度慢,容量有限
-
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
-
信号量:不能传递复杂消息,只能用来同步
-
共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了一块内存的。
管道(pipe)
-
仅用于父子进程间的通信
-
每个管道包含两个文件描述符, 一个用于读取一个用于写入
-
父子进程通常持有两个管道, 对于任一进程来说: 持有一个管道的写入描述符和另外一个管道的读取描述符
函数原型
#include <unistd.h>
int pipe (int fd[2]);
- f[0]用于读取, f[1]用于写入
代码示例
#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
void server(int readfd, int writefd)
{
char buffer[128] = { 0 };
int len = read(readfd, buffer, 128);
printf("server recv: %s\n", buffer);
strcpy(buffer, "this is server!");
write(writefd, buffer, strlen(buffer));
}
void client(int readfd, int writefd)
{
char buffer[128] = { 0 };
strcpy(buffer, "this is client!");
write(writefd, buffer, strlen(buffer));
int len = read(readfd, buffer, 128);
printf("client recv: %s\n", buffer);
}
int main()
{
int pipe1[2] = { 0 };
int pipe2[2] = { 0 };
pipe(pipe1);
pipe(pipe2);
pid_t pid = fork();
if(pid == 0)
{
close(pipe1[1]);
close(pipe2[0]);
//持有第一个管道用于读取的, 和第二个管道用于写入的
client(pipe1[0], pipe2[1]);
}
else
{
close(pipe1[0]);
close(pipe2[1]);
server(pipe2[0], pipe1[1]);
waitpid(pid, NULL, 0);
}
return 0;
}
资源消耗
-
每个文件需要两个文件描述符
-
如果没有数据的话可能会导致阻塞等待
有名管道(FIFO)
-
这是一个设备文件, 提供一个路径名与FIFO对应
-
不需要亲缘关系, 只要可以访问该路径名即可
-
与无名管道相比需要预先使用open打开
接口
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
-
pathname指的是路径名
-
mode指明权限, 文件权限是:
(mode & ~umask)
; 比如: 0666 -
使用unlink删除
示例(在两个文件中分别运行客户端和服务器)
#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFO_NAME "/tmp/test_fifo"
void server()
{
mkfifo(FIFO_NAME, 0666); //创建FIFO文件
int fd = open(FIFO_NAME, O_WRONLY);
printf("fd: %d\n", fd);
write(fd, "this is server!", 15);
close(fd);
unlink(FIFO_NAME);
}
void client()
{
int fd = open(FIFO_NAME, O_RDONLY);
printf("fd: %d\n", fd);
char buffer[128] = { 0 };
read(fd, buffer, 128);
printf("client recv: %s\n", buffer);
close(fd);
return;
}
int main()
{
server();
return 0;
}
- 如果采用: O_RDONLY 打开会阻塞, 直到有其他进程使用: O_WRONLY 打开; 反之亦然
信号
-
用户进程可以通过: signal/signalaction指定对信号的操作方式
-
可以对进程本身发送信号, 也可以对其他进程发送信号
常用接口
int kill(pid_t pid, int sig); //给指定进程发送信号
int raise(int sig); //向进程自己发送信号
unsigned int alarm(unsigned int seconds); //设置定时器
int pause(void); //挂起直到收到信号
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); //指定信号处理方式
void abort(void); //发送异常终止信号
简单示例
#include <sys/file.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <signal.h>
void handle(int sig)
{
printf("recv sig: %d\n", sig);
raise(SIGKILL);
}
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf("%s\n", "ss");
signal(SIGTERM, handle);
while(1) {};
//无限循环
}
else
{
sleep(2);
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
}
return 0;
}
内存映射
- 通过将某个设备映射到应用进程的内存空间, 通过直接的内存操作就可以完成对设备或文件的的读写
接口说明
- 1 头文件
#include <sys/mman.h>
- 2 创建内存映射
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
+ addr: 将文件映射到内存空间指定地址, NULL即可
+ length: 映射到内存空间的内存块大小
+ prot: 访问权限, PROT_EXEC| PROT_READ | PROT_WRITE | PROT_NONE
+ flags: 程序对内存块的改变有什么影响
+ MAP_SHARED, 共享的, 内存块修改会保存到文件, 默认这个就可以
+ MAP_PRIVATE, 私有的, 修改只在局部范围有效
+ MAP_FIXED, 使用指定的映射起始地址
+ MAP_ANONYMOUS/MAP_ANON, 父子进程可以使用匿名映射, 文件描述符-1即可
+ fd: 文件描述符
+ offset: 从文件的哪里开始, 默认0即可
+ 返回映射的指针地址
- 3 解除内存映射
int munmap(void *addr, size_t length);
+ addr: mmap的返回值
+ length: 长度
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
void server()
{
int fd;
char buffer[128] = { 0 };
strcpy(buffer, "this is server!");
fd = open("/tmp/mmap_temp_file", O_RDWR|O_CREAT|O_TRUNC, 0644);
ftruncate(fd, 64); //64的大小
// 使用fd创建内存映射区
void* addr = mmap(NULL, 64, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd); // 映射完后文件就可以关闭了
memcpy(addr, buffer, strlen(buffer)); // 往映射区写数据
munmap(addr, 64); // 释放映射区
}
void client()
{
int fd;
fd = open("/tmp/mmap_temp_file", O_RDONLY);
// 使用fd创建内存映射区
void* addr = mmap(NULL, 64, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
char *buffer = (char*)addr;
printf("%s\n", buffer);
munmap(addr, 64); // 释放映射区
}
int main() {
server();
return 0;
}
-
MAP_SHARED也会更新文件
-
MAP_PRIVATE 只能此进程访问此数据
消息队列
-
生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
-
消息队列可以双向通信
-
克服了管道只能承载无格式字节流的缺点;
-
从队列读出后会被删除
接口介绍
创建和访问一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
-
key, 消息队列的名称, 通过ftok产生
-
msgflag: IPC_CREAT和IPC_EXCL, 单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。
-
返回一个消息队列的标识码
ftok
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
-
pathname: 路径名
-
proj_id: 项目ID,非 0 整数(只有低 8 位有效)
消息格式
typedef struct _msg
{
long mtype; // 消息类型
char mtext[100]; // 消息正文
//…… …… // 消息的正文可以有多个成员
}MSG;
添加信息
#include <sys/msg.h>
int msgsnd( int msqid, const void *msgp, size_t msgsz, int msgflg);
-
msqid: 即msgget的返回值
-
msgp 待发送消息结构体的地址。
-
msgsz 消息正文的字节数。
-
msgflag: 默认0即可
获取信息
#include <sys/msg.h>
ssize_t msgrcv( int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg );
-
msqid:消息队列的标识符,代表要从哪个消息列中获取消息。
-
msgp: 存放消息结构体的地址。
-
msgsz:消息正文的字节数。
-
msgtyp:消息的类型。可以有以下几种类型:
- msgtyp = 0:返回队列中的第一个消息。
+msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)。
+msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
- msgtyp = 0:返回队列中的第一个消息。
-
在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进先出原则。
消息队列的控制
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
-
对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。
-
cmd:函数功能的控制。其取值如下:
- IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。
- IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构中。相对于,把消息队列的属性备份到 buf 里。
- IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值。相当于,消息队列原来的属性值清空,再由 buf 来替换。
-
buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。
代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
typedef struct _msg
{
long mtype;
char mtext[50];
}MSG;
void server()
{
struct _msg m = {100, "hihihih"};
key_t key;
int msgqid;
key = ftok("./", 2015);
msgqid = msgget(key, IPC_CREAT|0666); //额外指定权限
msgsnd(msgqid, &m, sizeof(m) - sizeof(long), 0);
//正文大小
}
void client()
{
key_t key = ftok("./", 2015);
int msgqid = msgqid = msgget(key, IPC_CREAT|0666);
struct _msg m;
msgrcv(msgqid, &m, sizeof(m) - sizeof(long), 100, 0);
msgctl(msgqid, IPC_RMID, NULL);
printf("%s\n", m.mtext);
return ;
}
int main() {
client();
return 0;
}
信号量
-
信号量是描述资源可用性的计数器; 信号量可以通过创建一个值为1的信号量来专门锁定某个对象, 如果信号量的值大于零,则资源可用, 进程分配“资源的一个单元”,信号量减少一个
-
传输的数据较少, 通常用于进程间同步; 可以和共享内存合作
接口
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
创建信号量集
int semget(key_t key,int nsems,int flags)
-
key: 和消息队列一样使用ftok获得即可
-
第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1
-
第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作(如果再配合IPC_EXEC则已存在会报错)
删除信号量
int semctl(int semid, int semnum, int cmd, ...);
-
semid: 信号量标识符
-
semnum: 当前信号量集的哪一个信号量
-
cmd通常是下面两个值中的其中一个
- SETVAL:用来把信号量初始化为一个已知的值, 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
- IPC_RMID:删除信号量标识符,删除的话就不需要缺省参数
-
第四个变量通常是: semun
union semun
{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
改变信号量的值
int semop(int semid, struct sembuf *sops, size_t nops);
-
nsops: 进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
-
sembuf的定义如下:
struct sembuf{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
代码实例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
void server()
{
key_t key;
int semid;
struct sembuf sb = { 0 };
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
key = ftok("./", 2015);
semid = semget(key, 1, IPC_CREAT | 0666); //创建信号量
printf("%s\n", "Get Sem!");
semop(semid, &sb, 1);
semctl(semid, 0, IPC_RMID); //删除
}
void client()
{
key_t key = ftok("./", 2015);
int semid = semget(key, 1, IPC_CREAT|0666);
struct sembuf sb = { 0 };
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
semop(semid, &sb, 1);
printf("%s\n", "Release Sem!");
return ;
}
int main() {
server();
return 0;
}
共享内存
- 使得多个进程可以访问同一块内存区域, 是最快可用的IPC形式, 往往和信号量结合使用, 达到进程间的同步与互斥
接口
创建共享内存
#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
-
key: 类似于semget/msget, 通过ftok获得的标识符
-
size: 指定共享内存大小, 它的值一般为一页大小的整数倍(如果不到一页会对齐到一页)
-
shmflag: 和semflag/msgflag一样, 指定权限; 此外通过IPC_CREAT | IPC_EXCL创建新的
将共享内存映射到虚拟地址空间
#include<sys/types.h>
#include<sys/shm.h>
void * shmat (int shmid, const void * shmaddr, int shmflg);
-
shmid: 标识符
-
shmaddr: 映射地址, NULL即可
-
shm_flg: 一组标志位,通常为0
操作共享内存
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
-
shm_id: 标识符
-
cmd: 三个值
- IPC_STAT: 把shmid_ds结构中的数据设置为共享内存的当前关联值, 即用共享内存的当前关联值覆盖shmid_ds的值。
- IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
- IPC_RMID:删除共享内存段(通常用这个就可以)
分离操作
int shmdt(const void *shmaddr);
- 注意, 并没有删除标识符和数据结构
代码实例(sem+shm)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
void server()
{
key_t sem_key = ftok("./", 2016); //信号量的key
int sem_id = semget(sem_key, 1, IPC_CREAT | 0666);
struct sembuf sb = { 0 };
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
semop(sem_id, &sb, 1);
//如果获取了信号量, 表示另外以测已经写入了
key_t shm_key = ftok("./", 2015);
int shmid = shmget(shm_key, 64, IPC_CREAT | 0666);
void *addr = shmat(shmid, NULL, 0);
printf("Get Sem & Get [%s]\n", (char*)addr);
semctl(sem_id, 0, IPC_RMID); //删除信号量
shmdt(addr);
shmctl(shmid, IPC_RMID, NULL); //删除共享内存
}
void client()
{
key_t key = ftok("./", 2015);
int shmid = shmget(key, 64, IPC_CREAT | 0666);
key_t sem_key = ftok("./", 2016);
int semid = semget(sem_key, 1, IPC_CREAT|0666);
struct sembuf sb = { 0 };
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
void *addr = shmat(shmid, NULL, 0);
strcpy((char*)addr, "this is client!");
semop(semid, &sb, 1);
printf("%s\n", "Write Data & Release Sem!");
shmdt(addr);
return ;
}
int main() {
server();
return 0;
}
几种通信方式比较
-
管道文件步占用磁盘空间, 管道读取完毕后会自动进入阻塞; 管道是半双工的, PIPE_SIZE限制为64k
-
管道是没有边界的, 只能传递无格式字节流
-
消息队列独立于进程外, 进程退出后数据依然存在; 要考虑上一次没有读完数据的问题
-
Linux下一个消息队列的最大字节数为 16k,系统中最多存在 16 个消息队列
-
消息队列在使用完后需要手动删除
-
信号/信号量: 不能用于传递复杂消息
-
共享内存: 是最快的, 因为少了将数据从用户态复制到内核态的拷贝过程; 缺乏同步安全
接口
很多接口都是类似的, 做一下总结
-
无名管道: pipe+write/read
-
有名管道: mkfifo + open(注意阻塞) + read/write + unlink
-
信号: kill+signal
-
内存映射(文件映射到内存): open+mmap+munmap
-
消息队列(通过key作为唯一标识): ftok+msgget+msgsnd+msgrcv+msgctl
-
信号量: ftok+semget+semop+semctl
-
共享内存: ftok+shmget+shmat+shmdt+shmctl