作者:华清远见讲师
对于进程来说,进程的本质就是程序的执行过程,进程是独立运行的单位,所以不同的程序的执行就产生不同的进程,进程和进程之间,运行空间时相互独立的,以平常的方法无法实现两者之间的通信。
这里给出几种进程之间通信的方法可供参考学习: 管道、信号、IPC通信共享内存、消息队列、信号量)、套接口(socket)。
刚刚接触进程的学者,我们可以给以上的方法分类,同时分析每种通信方式的实现过程:
第一类:原始通信方式:最早的通信方式,理解起来简单、易懂。
(1)管道通信:实质是管道文件操作,分为有名管道和 无名管道两种。
无名管道 : 用在有亲缘关系进程之间通信,例如父子进程之间。通信方向单一,有固定的读端口fd[0](只能read()),固定的写端口fd[1](只能write()),如下图所示,构成一个半双工通道。
图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从而实现通信
图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中通信方式,就能实现本机进程之间的通信。