作者:华清远见讲师

对于进程来说,进程的本质就是程序的执行过程,进程是独立运行的单位,所以不同的程序的执行就产生不同的进程,进程和进程之间,运行空间时相互独立的,以平常的方法无法实现两者之间的通信。

这里给出几种进程之间通信的方法可供参考学习: 管道、信号、IPC通信共享内存、消息队列、信号量)、套接口(socket)。

刚刚接触进程的学者,我们可以给以上的方法分类,同时分析每种通信方式的实现过程:

第一类:原始通信方式:最早的通信方式,理解起来简单、易懂。

(1)管道通信:实质是管道文件操作,分为有名管道和 无名管道两种。

无名管道 : 用在有亲缘关系进程之间通信,例如父子进程之间。通信方向单一,有固定的读端口fd[0](只能read()),固定的写端口fd[1](只能write()),如下图所示,构成一个半双工通道。

进程通信 python 进程通信的几种方法_华清远见

图1.无名管道

创建无名管道的方法:pipe()

包含头文件:#include

函数原型 : int pipe(int fd[2]);

fd: 整型的数组名数组长度2

返回值 : 成功 0 : fd[0] 读文件描述符 fd[1] 写文件描述符

失败 -1

举例:

#include
#include
#include
int main()
{
pid_t id;
int fd[2] = {0};
if(pipe(fd) == -1) // 注意1, pipe() 执行次数1次 放在 fork()前面
{
perror("pipe");
return -1;
}
id = fork(); // 创建一个子进程
if(id == 0) // 子进程
{
write(fd[1],"hello world!",13); // 写管道
printf("send OK\n");
}
else if(id > 0) // 父进程
{
char buf[100] = {0};
read(fd[0], buf, 100); // 读管道
printf("rcv : %s\n", buf);
}
else
{
perror("fork");
}
return 0;
}

有名管道: 用在任意两个进程之间通信,实质是两个进程同时访问一个管道文件,所有操作都属于文件IO。

创建有名管道方法:mkfifo()

头文件 :#include #include #include

函数原型 : int mkfifo(const char *filename, mode_t mode);

filename :管道文件的路径 相对路径 绝对路径

mode : 文件的访问权限

返回值 :成功 0

失败 -1 errno perror()

举例:

///读进程
#include
#include
#include
#include
#define FIFO "MYFIFO"
int main()
{
int r;
int fd;
char buf[100] = {0};
r = access(FIFO, F_OK); // 判断文件是否存在
if(r == -1) // 文件不存在
{
r = mkfifo(FIFO, 0666); // 创建管道文件
if(r == 0)
{
printf("create FIFO\n");
}
else //创建失败
{
perror("mkfifo MYFIFO");
return 0;
}
}
else //文件已存在
{
printf("%s 文件存在\n", FIFO);
}
fd = open(FIFO, O_RDONLY); //打开文件
if(fd != -1)
{
r = read(fd, buf, 100); //阻塞读取信息
printf("recv %d : %s\n", r, buf);
close(fd); // 关闭文件
}
return 0;
}
写进程
#include
#include
#include
#include
#include
#define FIFO "MYFIFO"
int main()
{
int fd;
fd = open(FIFO, O_WRONLY); //打开文件
if(fd != -1)
{ //写入信息
write(fd, "hello", strlen("hello")+1);
close(fd);
}
}

注意事项:管道通信, 两端(两个进程)通信, 要求 只有一端read() 方法, 另一端write()方法才有意义;read() 阻塞读取管道信息,write() 阻塞写入信息。

(2)信号:信号通信是唯一的异步通信,所有进程 默认接收所有信号

处理信号 : signal() 忽略信号 默认处理 捕捉信号(自定义处理方法)

发送信号 : 发送给目标进程 kill() 给自己 raise()

常见信号:

信号编号 信号名 默认意义 目的

2 SIGINT 按键 Ctrl+c 给终端上正在的进程 终止进程

3 SIGQUIT 按键 Ctrl+\ 给终端上正在的进程 终止进程

9 SIGKILL 杀死一个进程 (不能被忽略) 杀死进程

20 SIGTSTP 按键 Ctrl+z 终止进程

处理信号的函数:signal()

函数原型: #include

typedef void (*sighandler_t)(int); // 函数指针类型的重命名

函数原型 : sighandler_t signal(int signo, sighandler_t handler);

signo : 信号的编号

handler : SIG_IGN 忽略信号

SIG_DFL 默认处理

fun 自定义处理函数的首地址

返回值: 成功 首地址

失败 -1

发送信号的函数:kill() raise()

函数原型 : #include

#include

int kill(pid_t id, int signo);

id : 进程号 > 0 向PID== id 进程发送信号

0 向同组的进程发送信号

-1 向进程表中所有进程(除去 PID最大 )发送信号

< -1 向进程组号 == |id|发送信号

signo : 信号的编号 或信号名

返回值: 成功 0

失败 -1

int raise(int signo);

signo : 信号编号

返回值: 成功 0

失败 -1

举例:

接收信号的进程
#include
#include
void handler(int signo) // 自定义信号处理函数
{
if(signo == SIGQUIT) // 判断信号 是不是 SIGQUIT
{
printf("input ctrl+\\ \n");
}
else if(signo == SIGINT) // 判断信号 是不是 ctrl+c发送的
{
printf("Ctrl +c\n");
exit(0); //进程退出
}
}
int main()
{
signal(SIGINT, handler); // 设置信号处理方法
signal(SIGQUIT, handler);
while(1)
{
printf("PID = %d\n", getpid());
sleep(1);
}
发送信号的进程
#include
#include
#include
int main(int argc, char *argv[])
{
if(argc < 3)
{
printf("input: %s PID signo\n", argv[0]);
return 0;
}
pid_t id = atoi(argv[1]); // 获取参数 ---> 转成整型
int signo = atoi(argv[2]); // 信号编号 ---> 转成整型
kill(id, signo);
}

第二类:IPC通信方式, 通信的特点, 通过一个IPC对象,获取对应的ID从而实现通信

进程通信 python 进程通信的几种方法_进程通信 python_02

图2. IPC通信

获取IPC对象: 有亲缘进程之间, 例如 父子进程, IPC对象key 值 为IPC_PRIVATE

无关进程之间, key值 通过ftok() 获取:

方法: 函数原型: key_t ftok(const char *pathname, int idno);

pathname : 目录的路径 相对路径

绝对路径

idno : 取值 0 --- 255之间的数值

返回值:成功 键值 失败 -1

(1)共享内存

创建/打开共享内存-----> 映射------> 通信------> 解除映射 ----->删除共享内存

头文件:#include

#include

#include

创建/打开共享内存:

函数原型: int shmget(key_t key, int size, int shmflg);

key : 有亲缘关系进程 IPC_PRIVATE

无关进程 ftok() 获取键值

size : 共享内存的大小 字节数

shmflg : 共享内存的权限 和open() 的参数位相同

例子: key PIC_PRIVATE 0666 或者 0777 ....

ftok() IPC_CREAT|0666

返回值:成功 共享内存的段标识符

失败 -1

映射函数原型:

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid: 共享内存的段标志符

shmaddr : 计划映射的位置 == 首地址

NULL == 0 系统自动映射,找首地址

shmflg : 映射后空间的访问权限

SHM_RDONLY 只读

0 可读可写

返回值 :成功 映射区域的首地址

失败 (void *)-1

共享内存通信方法: 已知映射的首地址p 不阻塞读写------>效率高

读共享内存: printf("%s\n", p);

char buf[100]; strcpy(buf, p);

写共享内存: scanf("%s", p);

char buf[] = "jxkdadjlaj"; strcpy(p, buf);

解除映射 函数原型:

int shmdt(void *shmaddr);

shmaddr: 映射区域首地址

返回值: 成功 0

失败 -1

删除共享: int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid : 共享内存的段标志符

cmd: IPC_RMID 删除对象 第3个参数 NULL

IPC_SET 设置共享内存的属性 第3个参数 新属性存放位置

IPC_STAT 获取共享内存的属性 第3个参数 属性存放位置

buf : 属性存放的首地址

返回值: 成功 0

失败 -1

举例:

#include
#include
#include
#include
#include
#include
#include
int main()
{
int shmid;
pid_t id;
char *p;
shmid = shmget(IPC_PRIVATE, 100, 0666); // 创建/打开共享内存
if(shmid == -1)
{
perror("shmget");
}
else
{
printf("shmget OK\n");
system("ipcs -m"); // 查看共享内存信息
id = fork(); // 创建 子进程
if(id == 0) // 在 子进程中 写入信息
{
p = shmat(shmid, 0, 0); // 映射
if(p == (char *)-1)
{
perror("c: shmat");
exit(0);
}
printf("c: shmat OK\n");
strcpy(p, "hello"); // 写共享内存
shmdt(p); // 解除映射
}
else if(id > 0) // 在 父进程 中 读信息
{
p = shmat(shmid, 0, 0); // 映射
if(p == (char *)-1)
{
perror("f: shmat");
exit(0);
}
printf("f: shmat OK\n");
sleep(1); // 先 写入 再读取信息
printf("f : recv %s\n", p); // 读共享内存
shmdt(p); // 解除映射
shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
system("ipcs -m");
}
}
}

(2)消息队列

创建/打开消息队列 ---> 添加消息/取出消息 ----> 删除消息队列

头文件:#include

#include

#include

函数原型: int msgget(key_t key, int flag);

key : 键值 有亲缘关系进程 IPC_PRIVATE

无关 ftok()

flag : open()权限位 相同

0666 ....

IPC_CREAT | 0666

返回值:成功 消息队列ID

出错 -1

向消息队列中添加消息:msgsnd()

函数原型 : int smgsnd(int msgid, const void *buf, size_t size, int flag);

msgid : 消息队列ID

buf : 添加消息的存放位置

自定义结构体---->存放消息类型 + 正文

struct msgbuf

{

long type; // 消息的类型, 使用者给定的含义

正文数据类型 text[N]; // 根据具体情况改变的

}

size : 正文的字节数

flag : 0 阻塞形式添加消息

IPC_NOWAIT 不阻塞添加

返回值: 成功 0

失败 -1

从消息队列中取出消息:msgrcv() 过滤

函数原型 int msgrcv(int smgid, void *buf, size_t size, long msgtype, int flag);

msgid : 消息队列ID

buf : 接收到消息的存放位置

size : 正文字节数

msgtype : 接收的消息类型 过滤条件

0 没有过滤,直接接收队列的第一个消息

> 0 只接收 消息队列中,第一个消息类型 == msgtype 的消息

< 0 只接收 消息类型 不大于 |msgtype| 并且最小的

flag : 0 阻塞接收

IPC_NOWAIT 不阻塞接收

返回值: 成功 接收信息的实际字节数

失败 -1

删除消息队列: msgctl

int msgctl(int msgid, int cmd, struct msgid_ds *buf);

msgid : 消息队列ID

cmd : IPC_RMID 删除对象 第3个参数 NULL

IPC_SET 设置属性 第3个 buf 属性存放位置

IPC_STAT 获取属性 第3个 buf 属性存放位置

buf : 属性存放首地址

返回值: 成功 0 ; 失败 -1

举例:

#include
#include
#include
#include
#include
#include
#include
typedef struct msgbuf // 自定义消息的结构体类型
{
long type; // 消息类型
char text[100]; // 正文
}MSG_c;
int main()
{
pid_t id;
int msgid,r;
msgid = msgget(IPC_PRIVATE, 0666); // 创建/打开消息队列
if(msgid == -1)
{
perror("msgget");
return -1;
}
printf("msgget OK %d\n",msgid);
id = fork(); // 创建子进程
if(id == 0) // 子进程中 添加消息
{
MSG_c a;
a.type = 1; // 消息的具体内容
strcpy(a.text, "hello world!");
r = msgsnd(msgid, &a, sizeof(a.text), 0); // 阻塞形式添加到消息队列中
if(r == -1)
{
perror("msgsnd ");
exit(0);
}
printf("msgsnd OK r = %d\n",r);
}
else if(id > 0) // 父进程
{
MSG_c b;
r = msgrcv(msgid, &b, sizeof(b.text),1,0); // 取出第一个 消息类型== 1的消息
if(r == -1)
{
perror("msgrcv");
exit(0);
}
printf("r = %d,type = %ld %s\n", r, b.type,b.text); // 打印 消息信息
msgctl(msgid, IPC_RMID, NULL); // 删除消息队列
}
}

(3)信号量 :保护临界资源----> 进程之间实现互斥, 信号量常常修饰共享内存

创建/打开信号量---> 初始化信号量(执行 1 次)----> P操作 ----> V操作 --->删除

头文件:#include

#include

#include

创建一个信号量: semget()

函数原型: int semget(key_t key, int num, int semflg);

key : 键值 亲缘关系进程 IPC_PRIVATE

无关进程 ftok()

num : 信号量的个数 一般 1

semflg : 权限 亲缘进程 0666 ....

无关进程 IPC_CREAT

IPC_EXCL 唯一的信号量, 如果已经存在,则返回错误

返回值: 成功 信号量ID

失败 -1

函数原型: semctl()

int semctl(int semid, int semnum, int cmd, union semun arg);

semid : 信号量ID

semnum : 信号量的编号 一般 0 : 第一个信号量

cmd : IPC_STAT 获取这个semnum编号信号量的结构

IPC_RMID 删除信号量对象

IPC_SETVAL 设置信号量的值 val , 初始化 SETVAL

IPC_GETVAL 获取信号量的值

arg : 信号量相关结构 变量 , union 复用

自定义复用类型: union semun

{
int val;
struct semid_ds *buf;
unsigned short *arry;
};

返回值: 成功 0; 失败 -1

函数原型:semop()

int semop(int semid, struct sembuf *sp, size_t ns);

semid : 信号量ID

sp : 结构体变量的地址

系统定义好的结构体

struct sembuf
{
short sem_num; 信号量的编号 第一个信号量 0
short sem_op ; -1 P操作 ; 1 V操作
short sem_flg: 一般 SEM_UNDO
};

ns : 操作个数 一般 1

返回值: 成功 0 ; 失败 -1

一般开发者会自定义信号量的相关函数,如下:

a) 初始化信号量 : 自定义一个初始化函数 init_sem()

int init_sem(int semid, int no, int value)
{
union semun a; // 复用变量
a.val = value; // 初值
if(semctl(semid, no, SETVAL, a) == -1) // 设置信号量的值
{
return -1;
}
else
{
return 0;
}
}

调用: init_sem(semid, 0, 1);

b) P操作: 自定义一个P操作函数

int p_sem(int semid, int num)
{
struct sembuf mybuf;
mybuf.sem_num = num;
mybuf.sem_op = -1;
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0)
{
perror("semop");
exit(-1);
}
return 0;
}

c) V操作: 自定义一个V操作函数

int v_sem(int semid, int num)
{
struct sembuf mybuf;
mybuf.sem_num = num;
mybuf.sem_op = 1;
mybuf.sem_flg = SEM_UNDO;
if(semop(semid, &mybuf, 1) < 0)
{
perror("semop");
exit(-1);
}
return 0;
}

d) 删除信号量: 自定义一个删除信号量函数

int delete_sem(int semid,int no)
{
union semun a; // 复用变量
if(semctl(semid, no, IPC_RMID, a) == -1)
return -1;
else
return 0;
}

举例:

#include
#include
#include"mysem.h"
int main()
{
int semid;
pid_t id;
semid = semget(IPC_PRIVATE, 1, 0666); // 创建信号量
if(semid != -1)
{
printf("semget OK\n");
}
if(init_sem(semid, 0, 1) != -1) // 初始化信号量
{
printf("init_sem OK\n");
}
id = fork(); // 创建子进程
if(id == 0)
{
p_sem(semid, 0); // P操作
printf("child running\n");
sleep(2);
v_sem(semid, 0); // V操作
}
else if(id > 0)
{
sleep(1);
p_sem(semid, 0); //P操作
printf("father running\n");
v_sem(semid, 0); // V操作
delete_sem(semid, 0); // 删除信号量
}
}

第三类:socket套接口通信方式, 对于socket常用来实现网络中不同主机之间的进程间通信。这是另一个知识点,对于初学者只要掌握前5中通信方式,就能实现本机进程之间的通信。