一、学习内容
-
进程之间的通信(nterprocess communication)
-
信号通信
-
概念
1> 信号通信中,多个进程只起到通知作用,没有数据传输的功能
2> 所谓信号通信,就是软件模拟的硬件的中断请求
3>原理图
-
信号处理方式
-
默认(SIG_DFL)
停下正在执行的任务,去执行信号功能的任务,大多数默认处理是杀死进程 -
捕获
需要给定一个函数名,表示停下正在执行的任务,去执行给定的函数 -
忽略(SIG_IGN)
忽略当前信号,继续执行自己的任务
-
-
收到信号类型
-
内核发送过来的信号
如管道破裂时,内核向用户空间发射SIGPIPE信号,指针操作失误时内核向用户空间发射SIGSEGV信号 -
用户发送过来的信号
如用户键入ctrl+c,表示终端向该进程发送SIGINT信号;用户键入ctrl+z,表示终端向进程发送SIGSTOP信号; -
一个进程向另一个进程发送信号
需要使用相关函数完成,kill 函数,表示向某个进程发送信号
-
-
signal(信号绑定函数)
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);-
返回值
成功返回处理方式,失败返回SIG_ERR( void * -1 ),并置位错误码 -
参数
参数1:要处理的信号号
参数2:信号处理方式一共有三种
SIG_IGN:表示忽略该信号
SIG_DFL:表示默认处理该信号
自定义函数名:返回值为void,参数为int
-
功能
将发送给当前进程的信号与信号处理方式进行绑定
-
-
alarm(闹钟)
unsigned int alarm(unsigned int seconds);-
返回值
如果之前没有启动定时器,那么该函数返回0,如果之前启动了定时器,那么该函数返回上一个定时器剩余的秒数,并更新该定时器时间 -
参数
要延迟的秒数,如果为0,表示取消该定时器 -
功能
启动一个定时器,延迟seconds秒后,向该进程发送SIGALRM信号
-
-
kill(发信号)
int kill(pid_t pid, int sig);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
参数1:进程ID号
>0:表示向指定进程发送信号
=0:表示向当前进程所在的进程组发送信号
=-1:表示向所有进程发送信号
<-1:表示向进程号为pid的绝对值那个进程组发送信号参数2:要发送的信号号
-
功能
向指定的进程或进程组发信号
-
-
raise(发信号)
int raise(int sig);-
返回值
成功返回0,失败返回非0错误码 -
参数
要发送的信号号 -
功能
向当前进程发送信号(自己给自己发信号 kill(getpid(), sig))
-
-
-
System V提供的IPC通信方式
-
概念
system V提供的通信方式,是将通信容器独立于可执行程序而存在。即使相关程序已经退出,写入容器中的数据如果取出,依然存在 -
通信方式
消息队列
共享内存
信号量集
注意:
使用该通信方式时,需要先创建一个 IPC 对象,后续只需对该对象进行操作,就可以实现进程间通信
-
有关System V 提供的IPC对象操作的指令
1、ipcs:可以查看当前所有的消息队列、共享内存、信号量集的属性
2、ipcs -q:只查看消息队列的相关信息
3、ipcs -m:只查看共享内存的相关信息
4、ipcs -s:只查看信号量集的相关信号
5、ipcrm -q/-m/-s ID:表示删除某个消息队列、共享内存、信号量集
-
-
消息队列
-
原理图
-
key(钥匙)值的创建
key_t ftok(const char *pathname, int proj_id);-
返回值
成功返回当前ipc对象key值,失败返回-1并置位错误码 -
参数
参数1:一个必须已经存在的文件路径,占key值的四分之三,其中inode号占2字节,设备号占1字节
参数2:一个随机值,栈key值的1字节
-
功能
创建IPC通信的key值
-
-
API
-
创建消息队列
int msgget(key_t key, int msgflg);-
返回值
成功返回该消息队列的id号,以便于后期使用该消息队列,失败返回-1并置位错误码 -
参数
参数1:key值,可以由ftok创建出来,也可以使用IPC_PRIVATE(适用于亲缘进程间通信)
参数2:消息队列创建的标识
IPC_CREAT:表示创建一个消息队列,如果消息队列已经存在,则直接打开
IPC_EXEC:表示确保创建一个新的消息队列,如果该消息队列存在,则报错
注意:
如果是创建消息队列,那么该参数中也要包含该消息队列的权限 -
功能
通过给定的key值创建一个消息队列对象,并返回该消息队列的id号
-
-
向消息队列中存放数据
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
参数1:消息队列id号
参数2:消息的起始地址,一般是如下结构体类型,需要用户自己定义
struct msgbuf { long mtype; /* 消息类型,必须是大于0的数字,必须放在结构体的最上面 */ char mtext[1]; /* 消息正文 */ };
参数3:当前消息的正文的大小,不包含消息的类型大小
参数4:是否阻塞0:表示阻塞
IPC_NOWAIT:表示非阻塞 -
功能
向msqid对应的消息队列容器中存放以msgp作为起始地址的msgsz大小的数据
-
-
从消息队列中读取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);-
返回值
成功返回读取数据正文的大小,失败返回-1并置位错误码 -
参数
参数1:消息队列id
参数2:取出数据存放的容器起始地址
参数3:消息正文的大小
参数4:要取出的消息类型
>0:表示取得指定类型的消息的第一个
=0:表示取得消息队列的第一个(不限制类型)
<0: 从消息队列中取出类型为小于等于给定类型的绝对值的第一个消息
参数5:是否阻塞
0:表示阻塞
IPC_NOWAIT:表示非阻塞
-
功能
从消息队列中取出一条消息
-
-
消息队列的控制函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);-
返回值
成功返回0,失败返回-1并置位错误码 -
参数
参数1:消息队列id
参数2: 该函数要执行的操作指令,该函数会因不同的指令执行不同的操作
IPC_STAT: 获取当前消息队列的属性信息,并将该属性放入参数3中
IPC_SET: 设置当前消息队列的属性信息,需要将新信息当做第三个参数传入
IPC_RMID: 删除当前的消息队列,此时参数3可以省略填NULL即可
参数3:根据参数2而定
-
功能
用于控制消息队列的操作
-
-
-
-
-
脑图
二、作业
作业1:
使用消息队列实现两个进程的相互通信
代码解答:
方法一:进程法
进程A:
#include <myhead.h>
struct msgbuf
{
long mtype; // 消息类型,必须是长整型
char mtext[128]; // 消息正文,最多包含128个字符
};
#define MSGSZ (sizeof(struct msgbuf)-sizeof(long)) // 定义消息正文的大小,不包含消息类型
int main(int argc, const char *argv[])
{
key_t key = ftok("/", 'I'); // 使用ftok函数生成唯一的消息队列key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误的详细信息
return -1; // 返回错误
}
printf("key=%#x\n", key); // 打印生成的key
pid_t pid = fork(); // 创建子进程
if (pid > 0) // 父进程执行此部分代码
{
int msqid = msgget(key, IPC_CREAT | 0664); // 创建或获取消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
return -1; // 返回错误
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义发送消息的缓冲区
while (1)
{
printf("请输入消息类型>>>(类型只能有1)");
scanf("%ld", &buf.mtype); // 从用户输入中读取消息类型
getchar(); // 清除输入缓冲区中残留的换行符
if (buf.mtype != 1) // 如果消息类型不为1,提示重新输入
{
printf("输入的类型有误请重新输入\n");
continue;
}
printf("请输入消息正文>>>");
fgets(buf.mtext, MSGSZ, stdin); // 从标准输入中获取消息正文
buf.mtext[strlen(buf.mtext) - 1] = 0; // 去除换行符
msgsnd(msqid, &buf, MSGSZ, 0); // 将消息发送到消息队列
printf("发送成功\n"); // 成功发送提示
if (strcmp(buf.mtext, "quit") == 0) // 如果消息内容为"quit",退出循环
{
break;
}
}
}
else if (pid == 0) // 子进程执行此部分代码
{
int msqid = msgget(key, IPC_CREAT | 0664); // 获取父进程中创建的消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
return -1; // 返回错误
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf rbuf; // 定义接收消息的缓冲区
while (1)
{
msgrcv(msqid, &rbuf, MSGSZ, 2, 0); // 从消息队列中读取类型为2的消息,阻塞方式接收
printf("收到的消息为:%s\n", rbuf.mtext); // 打印接收到的消息内容
if (strcmp(rbuf.mtext, "quit") == 0) // 如果收到的消息内容为"quit",退出循环
{
break;
}
}
// 删除消息队列
if (msgctl(msqid, IPC_RMID, NULL) == -1) // 删除消息队列
{
perror("msgctl error"); // 打印删除消息队列时的错误信息
return -1; // 返回错误
}
exit(EXIT_SUCCESS); // 子进程正常退出
}
else // fork函数调用出错
{
perror("fork error"); // 打印fork调用错误信息
return -1; // 返回错误
}
waitpid(0, NULL, 0); // 父进程等待子进程退出
return 0; // 程序正常结束
}
进程B:
#include <myhead.h>
struct msgbuf
{
long mtype; // 消息类型,必须是长整型
char mtext[128]; // 消息正文,最多包含128个字符
};
#define MSGSZ (sizeof(struct msgbuf)-sizeof(long)) // 定义消息正文的大小,不包含消息类型
int main(int argc, const char *argv[])
{
key_t key = ftok("/", 'I'); // 使用ftok函数生成唯一的消息队列key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误的详细信息
return -1; // 返回错误
}
printf("key=%#x\n", key); // 打印生成的key
pid_t pid = fork(); // 创建子进程
if (pid == 0) // 子进程执行此部分代码
{
int msqid = msgget(key, IPC_CREAT | 0664); // 创建或获取消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
return -1; // 返回错误
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义发送消息的缓冲区
while (1)
{
printf("请输入消息类型>>>(类型只能有2)");
scanf("%ld", &buf.mtype); // 从用户输入中读取消息类型
getchar(); // 清除输入缓冲区中残留的换行符
if (buf.mtype != 2) // 如果消息类型不为2,提示重新输入
{
printf("输入的类型有误请重新输入\n");
continue;
}
printf("请输入消息正文>>>");
fgets(buf.mtext, MSGSZ, stdin); // 从标准输入中获取消息正文
buf.mtext[strlen(buf.mtext) - 1] = 0; // 去除换行符
msgsnd(msqid, &buf, MSGSZ, 0); // 将消息发送到消息队列
printf("发送成功\n"); // 成功发送提示
if (strcmp(buf.mtext, "quit") == 0) // 如果消息内容为"quit",退出循环
{
break;
}
}
}
else if (pid > 0) // 父进程执行此部分代码
{
int msqid = msgget(key, IPC_CREAT | 0664); // 创建或获取消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
return -1; // 返回错误
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf rbuf; // 定义接收消息的缓冲区
while (1)
{
msgrcv(msqid, &rbuf, MSGSZ, 1, 0); // 从消息队列中读取类型为1的消息
printf("收到的消息为:%s\n", rbuf.mtext); // 打印接收到的消息内容
if (strcmp(rbuf.mtext, "quit") == 0) // 如果收到的消息内容为"quit",退出循环
{
break;
}
}
// 删除消息队列
if (msgctl(msqid, IPC_RMID, NULL) == -1) // 删除消息队列
{
perror("msgctl error"); // 打印删除消息队列时的错误信息
return -1; // 返回错误
}
exit(EXIT_SUCCESS); // 父进程正常退出
}
else // fork函数调用出错
{
perror("fork error"); // 打印fork调用错误信息
return -1; // 返回错误
}
waitpid(0, NULL, 0); // 父进程等待子进程退出
return 0; // 程序正常结束
}
方法二:线程法
进程A:
#include <myhead.h>
// 定义消息缓冲区结构体,包含消息类型和消息正文
struct msgbuf
{
long mtmype; // 消息类型,必须是长整型
char mtext[128]; // 消息正文,最多128个字符
};
#define MSGSZ (sizeof(struct msgbuf) - sizeof(long)) // 计算消息正文的大小,不包含消息类型
// 发送消息到B进程的函数,作为父线程的执行体
void *send_to_b(void *arg)
{
key_t key = ftok("/", 'I'); // 使用ftok生成唯一的key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("key=%#x\n", key); // 打印生成的key
int msqid = msgget(key, IPC_CREAT | 0664); // 创建或获取消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义发送消息的缓冲区
while (1)
{
printf("请输入消息类型>>>(只能输入1)");
scanf("%ld", &buf.mtmype); // 获取消息类型
getchar(); // 清除输入缓冲区中的换行符
if (buf.mtmype != 1) // 如果消息类型不是1,提示重新输入
{
printf("输入有误,请重新输入\n");
continue; // 重新输入
}
printf("请输入消息正文>>>");
fgets(buf.mtext, MSGSZ, stdin); // 获取消息正文
buf.mtext[strlen(buf.mtext) - 1] = 0; // 去掉最后的换行符
msgsnd(msqid, &buf, MSGSZ, 0); // 将消息发送到消息队列
printf("发送成功\n"); // 提示发送成功
if (strcmp(buf.mtext, "quit") == 0) // 如果消息内容为"quit",退出循环
{
break;
}
}
pthread_exit(NULL); // 线程正常退出
}
// 接收来自B进程消息的函数,作为子线程的执行体
void *receive_from_b(void *arg)
{
key_t key = ftok("/", 'I'); // 生成唯一的key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("key=%#x\n", key); // 打印生成的key
int msqid = msgget(key, IPC_CREAT | 0664); // 获取或创建消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义接收消息的缓冲区
while (1)
{
msgrcv(msqid, &buf, MSGSZ, 2, 0); // 从消息队列中接收类型为2的消息
printf("收到消息为%s\n", buf.mtext); // 打印收到的消息内容
if (strcmp(buf.mtext, "quit") == 0) // 如果收到的消息为"quit",退出循环
{
break;
}
}
if (msgctl(msqid, IPC_RMID, NULL) == -1) // 删除消息队列
{
perror("msgctl error"); // 打印msgctl调用错误信息
pthread_exit(NULL); // 线程退出
}
pthread_exit(NULL); // 线程正常退出
}
// 主函数
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2; // 定义两个线程ID变量
pthread_create(&tid1, NULL, send_to_b, NULL); // 创建发送消息的父线程
pthread_create(&tid2, NULL, receive_from_b, NULL); // 创建接收消息的子线程
pthread_join(tid1, NULL); // 等待父线程执行完毕
pthread_join(tid2, NULL); // 等待子线程执行完毕
return 0; // 程序正常结束
}
进程B:
#include <myhead.h>
// 定义消息缓冲区结构体,包含消息类型和消息正文
struct msgbuf
{
long mtmype; // 消息类型,必须是长整型
char mtext[128]; // 消息正文,最多128个字符
};
#define MSGSZ (sizeof(struct msgbuf) - sizeof(long)) // 计算消息正文的大小,不包含消息类型
// 发送消息到A进程的函数,作为子线程的执行体
void *send_to_a(void *arg)
{
key_t key = ftok("/", 'I'); // 使用ftok生成唯一的key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("key=%#x\n", key); // 打印生成的key
int msqid = msgget(key, IPC_CREAT | 0664); // 创建或获取消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义发送消息的缓冲区
while (1)
{
printf("请输入消息类型>>>(只能输入2)");
scanf("%ld", &buf.mtmype); // 获取消息类型
getchar(); // 清除输入缓冲区中的换行符
if (buf.mtmype != 2) // 如果消息类型不是2,提示重新输入
{
printf("输入有误,请重新输入\n");
continue; // 重新输入
}
printf("请输入消息正文>>>");
fgets(buf.mtext, MSGSZ, stdin); // 获取消息正文
buf.mtext[strlen(buf.mtext) - 1] = 0; // 去掉最后的换行符
msgsnd(msqid, &buf, MSGSZ, 0); // 将消息发送到消息队列
printf("发送成功\n"); // 提示发送成功
if (strcmp(buf.mtext, "quit") == 0) // 如果消息内容为"quit",退出循环
{
break;
}
}
pthread_exit(NULL); // 线程正常退出
}
// 接收来自A进程消息的函数,作为父线程的执行体
void *receive_from_a(void *arg)
{
key_t key = ftok("/", 'I'); // 生成唯一的key
if (key == -1) // 错误检查
{
perror("ftok error"); // 打印ftok调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("key=%#x\n", key); // 打印生成的key
int msqid = msgget(key, IPC_CREAT | 0664); // 获取或创建消息队列
if (msqid == -1) // 错误检查
{
perror("msgget error"); // 打印msgget调用错误信息
pthread_exit(NULL); // 线程退出
}
printf("msqid=%d\n", msqid); // 打印消息队列ID
struct msgbuf buf; // 定义接收消息的缓冲区
while (1)
{
msgrcv(msqid, &buf, MSGSZ, 1, 0); // 从消息队列中接收类型为1的消息
printf("收到消息为%s\n", buf.mtext); // 打印收到的消息内容
if (strcmp(buf.mtext, "quit") == 0) // 如果收到的消息为"quit",退出循环
{
break;
}
}
if (msgctl(msqid, IPC_RMID, NULL) == -1) // 删除消息队列
{
perror("msgctl error"); // 打印msgctl调用错误信息
pthread_exit(NULL); // 线程退出
}
pthread_exit(NULL); // 线程正常退出
}
// 主函数
int main(int argc, const char *argv[])
{
pthread_t tid1, tid2; // 定义两个线程ID变量
pthread_create(&tid1, NULL, receive_from_a, NULL); // 创建接收消息的父线程
pthread_create(&tid2, NULL, send_to_a, NULL); // 创建发送消息的子线程
pthread_join(tid1, NULL); // 等待父线程执行完毕
pthread_join(tid2, NULL); // 等待子线程执行完毕
return 0; // 程序正常结束
}
成果展示:
方法一:进程法
方法二:线程法
三、总结
学习内容概述
1. 信号通信:
信号处理的三种方式(默认处理、捕获、忽略)、常见信号类型及其处理、如何使用 `signal` 函数绑定信号处理方式、通过 `alarm` 启动定时器,以及使用 `kill` 和 `raise` 发送信号给进程。
2. System V 提供的 IPC 机制:
包括消息队列的创建、发送、接收及控制函数的使用,重点学习了如何通过 `ftok` 函数生成消息队列的 `key`,以及消息队列的存取操作(`msgsnd` 和 `msgrcv`)。
学习难点
1. 信号处理的机制与应用:
1、理解信号的捕获机制和如何使用 `signal` 函数绑定信号处理函数是较为复杂的部分,尤其是不同信号的处理行为,如 SIGPIPE、SIGSEGV、SIGINT 等的触发条件和处理方式。
2、理解信号的实时性及不可排队处理。多个相同信号发送到一个进程时,可能会导致一些信号丢失,这要求处理程序具有足够的容错能力。
2. 消息队列的使用:
1、理解消息队列的结构体 `msgbuf` 的设计,以及消息类型 `mtype` 的特殊要求(必须为大于 0 的值)。
2、学习如何根据消息类型有选择地从消息队列中接收消息(通过 `msgrcv` 的 `msgtyp` 参数指定),并处理消息队列中的并发与顺序问题。
3、控制消息队列的操作,如 `msgctl` 用于删除或查看消息队列的状态,这在多进程协作中是一个重要但复杂的环节。
主要事项
1. 信号通信的使用:
信号的捕获与处理:
信号是操作系统发送给进程的异步通知,通过 `signal` 函数,进程可以指定自定义的信号处理函数来捕获并处理特定的信号。
常见信号处理:
例如 `SIGINT` 用于捕捉用户按下 `Ctrl+C` 产生的中断信号,`SIGPIPE` 用于处理写管道断裂,`SIGALRM` 是通过 `alarm` 函数生成的定时信号。
信号的发送:
通过 `kill` 可以向指定进程发送信号,`raise` 则用于向当前进程发送信号。信号可以用于进程间的简单通信和控制。
2. System V IPC 机制中的消息队列:
创建消息队列:
通过 `msgget` 函数创建消息队列,`ftok` 函数用于生成消息队列的唯一标识 `key`,确保不同进程可以共享同一个队列。
消息的发送与接收:
使用 `msgsnd` 向队列发送消息,使用 `msgrcv` 从队列中读取消息。每条消息都必须指定一个类型 `mtype`,用于标识消息的优先级或类别。
消息队列的控制:
`msgctl` 提供了一些管理消息队列的功能,如删除队列或查看队列状态,确保队列不再使用时能够清理资源。
未来学习的重点:
1. 深入理解信号的并发处理:
1、学习如何处理多个信号的并发情况,避免重要信号被忽略或丢失,特别是在高并发环境下如何避免信号混乱。同时学习如何使用 `sigaction` 替代 `signal` 进行更精细的信号处理控制。
2、学习更多系统信号的具体作用和场景应用,如 `SIGCHLD` 用于处理子进程退出,`SIGKILL` 强制结束进程等。
2. 高级 IPC 机制:
1、继续深入学习其他 System V IPC 机制,如共享内存(`shmget`、`shmat`)和信号量(`semget`、`semop`),这些是进程间通信的重要方式。
2、理解消息队列的并发访问控制,通过适当的同步机制(如信号量或锁)保证多进程之间的顺序操作和数据一致性。
3. 实战应用与优化:
1、将所学的信号处理机制与 IPC 机制结合应用于实际项目中,如创建一个简单的多进程任务管理系统,使用信号和消息队列进行进程间的协调和通信。
2、学习如何优化 IPC 性能,特别是在多进程并发较高的场景中,如何减少 IPC 操作的延迟,提升消息队列的处理效率。