原创首发于CSDN,转载请注明出处,谢谢! https://blog.csdn.net/weixin_46959681/article/details/114527183
什么是信号
实际上信号在操作系统中为 软中断 ,基本上所有的程序都需要处理信号。信号为计算机系统提供了一种处理异步事件的方法。例如,因程序编译运行错误导致终端打印大量乱码,此时仅需输入Ctrl + C
,计算机系统自身的信号机制会强制停止正在运行的错乱程序。
信号的处理
信号的处理有三种方法:
- 捕捉信号。告诉内核,用户希望如何处理某一种信号。编写一个信号处理函数,将该函数传给内核。当信号产生时,由内核来调用用户自定义的函数,实现某种信号的处理。另外,通过自定义编程的信号处理函数可以“修改”系统的默认动作(如
Crtl + C
,具体编程实例请看下文);- 忽略信号。大多数信号可使用该方式处理,但有两种信号不能被忽略(SIGKILL、SIGSTOP)。因其向内核和超级用户提供了进程终止和停止的可靠方法,若忽略则某个进程就变成了没人能管理的的进程;
- 系统的默认动作。所有被设计好的函数在系统中都有默认的处理动作,当产生该信号,系统会自动执行。实际工作中绝大部份的处理方式都直接了当,就是直接杀死该进程 “kill -9 pid” 。
|信号处理函数初级版 signal
函数原型:
#include <signal.h>
//实际的信号处理函数。
typedef void (*sighandler_t)(int);
//信号注册函数。signum为信号的编号,handler为中断函数的指针。
sighandler_t signal(int signum, sighandler_t handler);
通过信号处理的初级函数 signal ,以指令 Ctrl + C
为例实现
信
号
捕
捉
信号捕捉
信号捕捉 与
默
认
动
作
修
改
默认动作修改
默认动作修改。
演示代码:signal1.c
/* signal1.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signum)
{
printf("Get signum = %d.\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 10:
printf("SIGUSR1\n");
}
printf("Never quit.\n");
}
int main(){
signal(SIGINT,handler);
signal(SIGUSR1,handler);
while(1);
return 0;
}
运行以上代码,当输入 Ctrl + C 时终端显示 signum = 2 ,能看到系统默认的强制退出动作经过以上编写的自定义信号函数后“无效”了,唯有通过指令 k i l l kill kill 杀死该进程。
输入 Ctrl + C 强制退出时 | 使用 kill 命令处理 |
---|---|
|信号处理函数高级版 sigaction & sigqueue
s i g n a l signal signal 是初级的信号处理函数,仅能发出处理的“动作”。若是要进一步携带其他消息,如 信号值、字符串、PID号、时钟、内存地址 等等内容,则需要更高级的信号处理函数 s i g a c t i o n sigaction sigaction 、 s i g q u e u e sigqueue sigqueue。
对于这两个高级信号处理函数的构造、具体参数等等细节笔者还在摸索阶段,请读者自行使用 man 手册以及阅读参考资料里的博文 —— Linux 信号(signal)。笔者这里直接贴出教学视频里的两段代码,感兴趣的读者请细细品味。
第一步:运行接收端代码文件 sigaction.c
生成的编译文件 receive
。
/* sigaction.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
//发送端所发的一切信息都可用指针info读取。
void handler(int signum, siginfo_t *info,void *context)
{
printf("Get signum %d\n",signum);
//接收信号并判断内容是否为空。
if(context != NULL){
printf("Get data = %d\n",info->si_int);
//printf("Get data = %d\n",info->si_value.sival_int);
printf("From Send's pid = %d\n",info->si_pid);
}
}
int main()
{
struct sigaction act;
printf("Receive's pid = %d\n",getpid());
act.sa_sigaction = handler;
//宏,表示能接收数据。
act.sa_flags = SA_SIGINFO;
//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
第二步:运行发送端代码文件 sigqueue.c
生成的编译文件 send
。
/* sigqueue.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int signum;
int pid;
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 10000;
//int sigqueue(pid_t pid, int sig, const union sigval value);
sigqueue(pid,signum,value);
printf("Send's pid = %d\n",getpid());
printf("Over.\n");
return 0;
}
运行结果:
发送端 | 接收端 |
---|---|
信号量
信号量(semaphore) 与笔者在前两篇博文中介绍的通信机制 —— 管道 、消息队列与共享内存 —— 不同, 本质上可视为一个 计数器 。信号量具备初始值,若 初始值 > 0,说明其处于“空闲状态”;若 初始值 < 0,说明其处于“占用状态”。当其处于“占用状态”时,不论是进城或线程,都要处于等待状态,等待被“唤醒”(PV操作)。
信号量用于实现系统进程间的互斥与同步以及同一进程的不同线程的同步,而不是用来存储进程间的通信数据。
拓展:
- 同步:处理 Linux 系统里进程的竞争就是同步,安排进程执行的先后顺序就是同步,每个进程都有一定的个先后执行顺序;
- 互斥:访问不可共享的临界资源(如物理设备),会引发两个新的控制问题(互斥可以被说成特殊的同步吗?);
- 竞争:当父子进程又或者并发进程竞争使用同一个资源的时候,我们就称为竞争进程。
函数原型:
//创建一个信号量组,成功返回信号量集ID,失败返回-1。
int semget(key_t key,int nsems,int semflg);
//对信号量组进行操作,改变信号量的值(PV操作)。
int semop(int semid, struct sembuf *sops, size_t nsops);
//控制信号量的相关信息。
int semctl(int semid, int semnum, int cmd, ...);
上文中的
P
V
PV
PV 操作笔者在这里给出一小段含 fork()
函数的演示代码,对于代码中更加细节的部分如 sembuf 等,已经有大量的博客给出了相应的解释和实例代码(请看最下行的参考博文),这里不再多费笔墨。
演示代码:semaphore.c
/* semaphore.c */
#include <stdio.h>
#include <error.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
void sem_p(int sem_id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = -1;
set.sem_flg = SEM_UNDO;
semop(sem_id,&set,1);
printf("sem_p OK.\n");
}
void sem_v(int sem_id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg = SEM_UNDO;
semop(sem_id,&set,1);
printf("sem_v OK.\n");
}
int main(int argc, char **argv[])
{
int sem_id;
pid_t pid;
key_t key;
key = ftok(".",'o');
printf("key = %x\n",key);
//创建信号量。
sem_id = semget(key,1,IPC_CREAT|0666);
//设置联合体内信号量的值,初始化信号量组以及信号量值为零。
union semun initsem;
initsem.val = 0;
semctl(sem_id,0,SETVAL,initsem);
pid = fork();
if(pid > 0){//父进程。
sem_p(sem_id);
printf("Process father: pid = %d.\n",getpid());
sem_v(sem_id);
semctl(sem_id,0,IPC_RMID);//删除信号量集。
printf("sem_del OK.\n");
}else if(pid == 0){//子进程。
printf("Process child: pid = %d.\n",getpid());
sem_v(sem_id);
}else{
printf("Fork fail.\n");
}
return 0;
}
运行结果:
主函数内调用函数 f o r k ( ) fork() fork() 产生父子进程,当父进程运行时因演示代码中联合体内的信号量初始值为零 i n i t s e m . v a l = 0 initsem.val = 0 initsem.val=0 ,无法执行 P 操作 sem_p 进入等待状态,系统进程跳转先执行子进程。子进程执行完毕后,经过完整的 P V PV PV 操作,父进程得以继续执行完毕。
笔者没有学习过计算机操作系统,上述一段只能从代码层面自圆其说,显得过于粗浅。
参考资料
- 参考博客 进程间通信的五种方式
- 参考博客 进程间的五种通信方式介绍-详解
- 知乎文章 进程间通信的方式(四):信号量
文章更新记录
- 文章框架完成。「2021.3.8 12:05」
- “信号的处理”一节完成。 「2022.3.15 11:21」
- “信号处理函数高级版 sigaction & sigqueue”一节完成。「2022.3.17 22:50」
- “信号量”一节完成。「2021.3.19 17:24」
P.S. 《计算机操作系统》与《计算机组成原理》是每一个嵌入式开发者必备的计算机内功心法。