前面几篇的TCP、UDP协议的socket主要为跨设备进行数据交换的网络通信,而本篇则将介绍单机系统内多进程间协作的进程间通信IPC(Inter-Process Communication),由于IPC主要分为管道,消息队列,信号量,共享内存,信号,套接字(绑定本地ip),因此本篇主要对单工的信号进行详解。
一、信号处理
在 Linux 操作系统中,信号是一种软件中断机制,用于处理异步事件。信号可以由一个进程发送给另一个进程(或者同一个进程内的不同线程),用于通知接收进程发生了某种特定的事件。
1.1 信号的类型
以可移植操作系统接口(Portable Operating System Interface of UNIX)POSIX.1的2008标准为例如下图:
SIGHUP
(Hang Up) - 终端断开连接时触发,通常用于通知程序重新读取配置文件或优雅地关闭。
SIGINT
(Interrupt) - 程序被中断信号,通常由用户发送 (Ctrl+C)。
SIGQUIT
(Quit) - 程序被退出信号,通常由用户发送 (Ctrl+\),会生成核心转储用于调试。
SIGILL
(Illegal Instruction) - 程序执行了非法指令。
SIGTRAP
- 由断点指令或调试器捕获时触发。
SIGABRT
(Abort) - 程序异常终止,通常由abort()
函数调用触发。
SIGBUS
(Bus Error) - 硬件故障,如访问了一个非法内存地址。
SIGFPE
(Floating Point Exception) - 浮点异常,如除以零。
SIGKILL
- 立即终止进程,无法被捕获或忽略。
SIGUSR1
- 用户定义的信号,用途可以自定义。
SIGSEGV
(Segmentation Fault) - 程序试图访问其内存空间中未分配或无法访问的内存。
SIGUSR2
- 用户定义的信号,用途可以自定义。
SIGPIPE
- 写入一个已被关闭的管道。
SIGALRM
(Alarm Clock) -alarm()
函数设置的定时器信号。
SIGTERM
- 默认的终止信号,可以被捕获和处理,用于请求程序优雅地终止。
SIGSTKFLT
- 栈溢出信号。
SIGCHLD
(Child Process) - 子进程停止或退出时触发。
SIGCONT
(Continue) - 继续执行一个之前被停止的进程。
SIGSTOP
- 停止进程,无法被捕获、忽略或通过代码发出。
SIGTSTP
(Terminal Stop) - 终端停止信号,通常由 Ctrl+Z 触发。
SIGTTIN
- 后台进程试图从控制终端读数据。
SIGTTOU
- 后台进程试图写入控制终端。
SIGURG
(Urgent Data) - 套接字上有紧急数据要读取。
SIGXCPU
(Exceeded CPU time limit) - 超过 CPU 时间限制。
SIGXFSZ
(File size limit exceeded) - 超过文件大小限制。
SIGVTALRM
(Virtual Timer Alarm) -setitimer()
函数设置的定时器信号。
SIGPROF
(Profiling Timer Alarm) - 由setitimer()
函数设置的定时器信号,用于性能分析。
SIGWINCH
(Window Change) - 终端窗口大小发生变化。
SIGIO
/SIGPOLL
- 输入/输出现在可能,或可轮询事件。
SIGPWR
(Power Failure) - 电源故障。
SIGSYS
(Bad System Call) - 非法系统调用。SIGRTMIN
到SIGRTMAX
- 为实时信号,用于用户定义的信号,可以安全地排队,并且可以由实时调度策略中的进程使用。这些信号允许应用程序定义自己的信号和处理方式,而不会影响到标准的 POSIX 信号。
1.2信号的使用
1.2.1 bash指令
使用kill指令进行信号的发送
kill [options] <pid> [...]
options:一般为-<signal> 或者 -s <signal>,signal也可以为信号编号
pid:进程id,可以通过ps或ps -a来显示
随便写一个无限循环进行运行(记得加sleep),再开一个会话用kill发送SIGINT
信号,效果等同于原会话键盘输入
Ctrl+C终止a.out的运行
1.2.2 指针函数与函数指针
在介绍后面的signal函数之前先介绍一下指针函数与函数指针二者的区别。
○ 指针函数:本质为函数,返回值为指针类型
一般写法为:
int *fun(int x,int y);
示例 :
#include <stdio.h>
#include <stdlib.h>
typedef struct customData
{
int a1;
int b1;
int sum;
} Data;
Data *add(int a2, int b2)
{
// 直接使用未初始化的指针会导致段错误
Data *data1 = (Data *)malloc(sizeof(Data));
if (data1 == NULL)
{
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
data1->a1 = a2;
data1->b1 = b2;
data1->sum = a2 + b2;
return data1;
}
int main()
{
Data *data2 = add(3, 4);
printf("%d %d %d\n", data2->a1, data2->b1, data2->sum);
free(data2);
return 0;
}
展示:
○ 函数指针:本质为指针变量,指针指向一个函数
一般写法为:
int (*fun)(int, int);
示例:
//回调函数是一种以函数指针作为参数的函数,它允许在另一个函数中调用这个指针所指向的函数
#include <stdio.h>
#include <stdlib.h>
int c1 = -1, c2 = -1;
int mul(int a, int b)
{
return a * b;
}
int callback1(int a, int b, int (*fun1)(int, int))
{
c1 = (*fun1)(a,b);
printf("%d\n",c1);
return (c1 ? 1 : 0);//三目运算符,等同于if(c1 > 0) return 1; else return 0;
}
//第二种写法
typedef int (*Fun)(int, int);
int callback2(int a, int b, Fun fun2)
{
c2 = fun2(a,b);
printf("%d\t",c2);
return (c2 ? 1 : 0);
}
int main()
{
printf("[callback1_ret:%d]\n[callback2_ret:%d]\n", callback1(2,3,mul), callback2(2,3,mul));
return 0;
}
展示:
1.2.3 signal函数
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*****************************************************************************
函数名称 : signal
功能描述 : 设置一个函数来处理某一个信号
输入参数 : signum: 信号名或信号名对应的整型数字
handler: 函数指针sighandler_t类型的对象,指向一个处理函数
输出参数 : 无
返 回 值 : sighandler_t 该函数返回指向之前信号处理函数的指针,当发生错误时返回 SIG_ERR
*****************************************************************************/
这时我们便可以写一个简单的函数来实现CTRL+C信号的发送
//发送端,000.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(){
int i = 0;
char cmd[128];
//killall 向指定名称的程序发送信号
//也可以直接使用int kill(pid_t pid, int sig)函数
sprintf(cmd, "killall -%d a.out", SIGINT);
while(i < 3){
//system 运行shell命令,传入参数类型为char*
system(cmd);
i++;
sleep(1);
}
return 0;
}
//接收端,001.c a.out
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
//信号处理函数
int count = 1;
void int_handler(int signum){
printf("revice signal %d\n", signum);
printf("count: %d\n",count++);
}
int main(){
int i = 0;
signal(SIGINT, int_handler);
while(i < 100){
sleep(3);
}
return 0;
}
展示:
可以看到,无论是000程序发送的信号2(count 1,2,3)还是自身键盘输入的CTRL+C(count 4,5),a.out程序都进行了处理,最后输入CTRL+\进行了当前程序的退出。现在信号的发送与处理你已经会了,那对于这64个信号都是一样的操作嘛,它们之间是否有所区别呢?
1.2.4 传统信号与实时信号的区别
除了 kill() 进行信号的发送,signal() 进行信号的处理外,还有两个函数也具备相同的功能,它们便是 sigaction() 和 sigqueue()
SYNOPSIS
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
功能:用于检查或修改与特定信号相关联的处理动作
参数:
signum: 信号编号
act: 指向sigaction结构体的指针,用于具体的信号处理动作
oldact: 保存旧的信号处理动作的指针
参数:
sa_handler: 信号处理函数指针
sa_sigaction: 信号处理函数指针,比前者多了数据值,第三参数不使用
siginfo_t *: 指向siginfo_t结构体的指针,储存信号的详细数据
sa_mask: 设置阻塞信号集
int sigemptyset(sigset_t *set): 将某个信号集清0
int sigaddset(sigset_t *set, int signum): 将某个信号加入信号集
int sigdelset(sigset_t *set, int signum): 将某个信号清出信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset):信号屏蔽处理
how: 操作参数
SIG_BLOCK:set表示需要屏蔽的信号集
SIG_UNBLOCK:set表示需要解除屏蔽的信号集
SIG_SETMASK:set表示用于替代原始屏蔽集的新屏蔽集
set: 传入要处理的信号集
oldset: 传出旧的信号屏蔽集
sa_flags: 信号动作选项
SA_SIGINFO: 信号处理函数改为sa_sigaction,sa_flags设置为0使用sa_handler
SA_NODEFER: 在信号处理函数执行期间不会屏蔽当前信号
SA_RESTART: 使被信号中断的系统调用自动重新启动
sa_restorer: 过时不使用
SYNOPSIS
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
函数功能: 向一个进程发送信号和数据
参数解释:
pid: 进程号id
sig: 信号id
value: sigval共用体变量(共用内存),用于指定信号传递参数
//联合体的⼤⼩⾄少是最⼤成员的⼤⼩(如本例sigval为4字节)
//当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍(int; char[5]; 联合大小即为8字节)
sival_int: 携带的整型数据
sival_ptr: 携带的数据指针
虽然这两对函数都能实现相同的功能,但它们总归有所区别,对于sigqueue
和 kill来说,
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组,也不能发送 SIGKILL
和 SIGSTOP
信号
对于sigaction与signal来说,sigaction
提供了更多的控制,包括信号集的屏蔽和额外的信号处理选项,也比signal更加稳定和可靠,而signal相对实现较简单,且用signal函数注册的信号处理函数只会被调用一次,之后收到这个信号将按默认方式处理
对比函数区别后,接下来便是对比传统信号与实时信号了。
//发送端 000.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[]){
//参数判断
if(argc != 3) {
fprintf(stderr, "%s lost send pid or signal num as argvs\n", argv[0]);
exit(0);
}
//设置发送信号及携带信息
//int atoi(const char *str)将str指针转换为int
pid_t pid = atoi(argv[1]);
int Match_sig = atoi(argv[2]);
union sigval sival;
sival.sival_int = 1314;
//循环发送
int i = 0;
while(i < 3){
sigqueue(pid, Match_sig, sival);
i++;
}
return 0;
}
//接收端 001.c a.out
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
//信号处理函数统计处理次数
int count = 1;
void int_handler(int signum, siginfo_t *info, void *ucontext){
printf("revice sig= %d data= %d from %d\n", signum, info->si_int, info->si_pid);
printf("count: %d\n",count++);
}
int main(int argc, char** argv){
//初始化
int i = 0;
printf("self pid:%d\n",getpid());
if(argc != 2) {
fprintf(stderr, "%s lost signal num as argv1\n", argv[0]);
exit(0);
}
int Match_sig = atoi(argv[1]);
//信号处理函数选择sa_sigaction
struct sigaction siga;
siga.sa_sigaction = int_handler;
siga.sa_flags = SA_SIGINFO;
//设置信号屏蔽
sigset_t myset;
sigemptyset(&myset);
sigaddset(&myset, Match_sig);
sigprocmask(SIG_BLOCK, &myset, NULL);
siga.sa_mask = myset;//非必要
//关联并阻断信号
sigaction(Match_sig, &siga, NULL);
while(i < 5){
printf("start of lost time %d s\n", i++);
sleep(3);
}
//解除屏蔽
//若为sigemptyset或sigfillset则此时应选SIG_SETMASK
sigprocmask(SIG_UNBLOCK,&myset,NULL);
return 0;
}
展示:
传统信号(标准信号):2)SIGINT
实时信号:40) SIGRTMIN+6
在处理传统信号时,多个相同类型的信号在前一个信号未处理完时会被丢弃;而在处理实时信号时,多个相同类型的信号会被排队等待处理。
本篇主要对ipc通信的信号进行了描绘,了解了两种信号发送和接收函数的区别以及传统信号与实时信号的不同,那么问题来了,既然同种信号的传统会被丢弃,那么不同种信号连续发送又会对传统信号和实时信号造成什么样的结果呢?