Linux进程间通信 - 信号
0. 前言
进程是一个独立的资源管理单元,不同进程间的资源是独立的,不能在一个进程中访问另一个进程的用户空间和内存空间。但是,进程不是孤立的,不同进程之间需要信息的交互和状态的传递,因此需要进程间数据的传递、同步和异步的机制。
当然,这些机制不能由哪一个进程进行直接管理,只能由操作系统来完成其管理和维护,Linux提供了大量的进程间通信机制,包括同一个主机下的不同进程和网络主机间的进程通信,如下图所示:
- 同主机间的信息交互:
- 无名管道:
特点:多用于亲缘关系进程间通信,方向为单向;为阻塞读写;通信进程双方退出后自动消失
问题:多进程用同一管道通信容易造成交叉读写的问题- 有名管道:
FIFO(First In First Out),方向为单向(双向需两个FIFO),以磁盘文件的方式存在;通信双方一方不存在则阻塞- 消息队列:
可用于同主机任意多进程的通信,但其可存放的数据有限,应用于少量的数据传递- 共享内存:
可实现同主机任意进程间大量数据的通信,但多进程对共享内存的访问存在着竞争
- 同主机进程间同步机制:信号量(Semaphore)
- 同主机进程间异步机制:信号(Signal)
- 网络主机间数据交互:Socket(套接字)
1. 概念
名词解释:
- ①. 发送信号:
包括一个进程向自己发送信号、向其它进程发送信号、内核向用户进程发送信号- ②. 安装中断:
当某个信号到来时,执行用户定制的中断服务程序,而非默认操作- ③. 递送信号:
一个信号被操作系统发送到目标进程;所有信号的传递必须经过操作系统这一层- ④. 捕获信号:
信号在目标进程中引起某段处理程序的执行- ⑤. 屏蔽信号:
进程告诉操作系统,暂时不接受某些信号:屏蔽期间被屏蔽信号不会被进程捕获;当解除屏蔽后,在屏蔽期的发送的屏蔽信号将会被捕获到- ⑥. 忽略信号:
目标进程不处理忽略信号,直接丢弃- ⑦. 未决信号:
屏蔽期的被发送到目标进程的未被捕获的屏蔽信号- ⑧. 可靠与不可靠信号:
可靠信号:编号大于32的信号,屏蔽期收到多次同一信号,会记录次数,屏蔽结束后,捕获相同次数的信号;
不可靠信号:编号小于32的信号;同上,但是只捕获一次“未决信号”
信号定义:
在/usr/include/bits/signum.h
中,对信号宏进行了定义:
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31
信号及其默认操作如下(信号都可以唤醒被中断的进程)(以字母排序):
Signal | Signum | Description | Default action | Comment |
---|---|---|---|---|
SIGABRT | 6 | 异常终止 | 终止 | |
SIGALRM | 14 | 超时 | 终止 | |
SIGBUS | 7 | 硬件故障 | 终止 | |
SIGCHLD | 17 | 子进程状态改变 | 忽略 | 子进程退出时发送该信号给父进程 |
SIGCLD | 16 | 栈错误 | 终止 | |
SIGCONT | 18 | 唤醒暂停进程继续工作 | 忽略/继续 | 与SIGSTOP配合使用 |
SIGFPE | 8 | 算数异常 | 终止 | |
SIGHUP | 1 | 链接断开 | 终止 | |
SIGILL | 4 | 非法硬件指令 | 终止 | |
SIGINT | 2 | 终端中断符 | 终止 | |
SIGIO | 29 | 异步I/O | 终止 | |
SIGIOT | 6 | 硬件故障 | 终止 | |
SIGKILL | 9 | 终止 | 终止 | 不可被屏蔽、安装 |
SIGPIPE | 13 | 写至无读进程的管道 | 终止 | |
SIGPOLL | 29 | 可轮询事件 | 终止 | |
SIGPROF | 27 | 时间超时 | 终止 | |
SIGPWR | 30 | 电源失效/重启 | 忽略 | |
SIGQUIT | 3 | 终端退出符 | 终止 | |
SIGSEGV | 11 | 无效存储访问 | 终止 | |
SIGSTKFLT | 16 | 栈错误 | 终止 | |
SIGSTOP | 19 | 停止作业 | 暂停进程 | 不可被屏蔽、安装 |
SIGSYS | 31 | 无效系统调用 | 终止 | |
SIGTERM | 15 | 终止进程 | 终止 | |
SIGTRAP | 5 | 硬件故障 | 终止 | |
SIGTSTP | 20 | 终端挂起 | 停止进程 | |
SIGTTIN | 21 | 后台从控制tty读作业 | 停止进程 | |
SIGTTOU | 22 | 后台向控制tty写作业 | 停止进程 | |
SIGURG | 23 | 紧急情况 | 忽略 | |
SIGUSR1 | 10 | 用户自定义信号 | 终止 | |
SIGUSR2 | 12 | 用户自定义信号 | 终止 | |
SIGVTALRM | 26 | 虚拟时间闹钟(setitimer) | 终止 | |
SIGWINCH | 28 | 终端窗口大小改变 | 忽略 | |
SIGXCPU | 24 | 超过CPU限制(setrlimit) | 终止 | |
SIGXFSZ | 25 | 超过文本长度限制(setrlimit) | 终止 |
2. 信号生命周期
1). 安装信号
当进程收到一个信号时,可采取三种处理方式:
- 忽略此信号:SIGKILL和SIGSTOP不可被忽略,其它均可
- 执行默认操作:如上表所示
- 用户自定义操作:要求必须首先安装该信号
安装信号的方式有两种:
- signal():
- sigaction():
①. signal()
作用:
针对某个信号,安装用户自定义的操作函数头文件:
#include <signal.h>
函数原型:
sighandler_t signal(int signum, sighandler_t handler)
参数:
signum: 信号的编号,如上表所示
handler: typedef void (*sighandler_t)(int), 即安装的中断处理函数
可以设置为三种类型
宏参数 int 描述 SIG_ERR -1 返回错误 SIG_DFL 0 执行该信号的默认操作 SIG_IGN 1 忽略该信号 handler 用户自定义中断处理函数
- 返回值
成功:handler以前的值
失败:SIG_ERR- 其它说明:
②. sigaction()
作用:
更强大的安装信号,检查和更改信号的处理操作头文件:
#include <signal.h>
函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
参数:
signum: 信号的编号,如上表所示
act: 指定欲设置的信号处理方式
oldact: 存储执行此信号前针对此信号的安装信息struct sigaction { union { __sighandler_t _sa_handler; //SIG_DFL、SIG_IGN void (*_sa_sigaction)(int, struct siginfo *, void *); //信号捕获函数 } _u; sigset_t sa_mask; //执行信号捕获函数时,添加到进程屏蔽信号集中的信号集 unsigned long sa_flags; //影响信号行为的特殊标志 void (*sa_restorer)(void); }; #define sa_handler _u._sa_handler #define sa_sigaction _u._sa_sigaction
- 返回值
成功:0
失败:-1
2). 产生信号
信号的发送是从一个进程产生,经由OS,发送到另一个进程(或自身).
发送信号有三种方式:
- kill(): 发送信号到一个指定进程
- raise(): 自产生一个信号
- alarm():
①. kill()
作用:
传送一个信号到指定进程头文件:
#include <signal.h>
函数原型:
int kill(pid_t pid, int signum)
参数:
pid: 目标进程的ID
PID Description PID > 0 将信号发送到该PID的进程中 PID = 0 将信号发送到与当前进程同一进程组的所有进程 PID = -1 将信号发送给系统内的所有进程 PID < 0 将进程发送给进程组好PGID为PID绝对值的所有进程 signum: 发送的信号编号
- 返回值
成功:0
失败:-1
②. raise()
作用:
给当前进程发送信号,即唤醒进程头文件:
#include <signal.h>
函数原型:
int raise(int signum)
参数:
signum: 发送的信号编号
- 返回值
成功:0
失败:-1
③. alarm()
作用:
产生一个定时信号SIGALRM(只产生一次,并非循环产生)头文件:
#include <unistd.h>
函数原型:
int alarm(unsigned int seconds)
参数:
seconds: 延迟时间
返回值
成功:0
失败:-1相似函数:
useconds_t ualarm(useconds_t usecs, useconds_t interval) //usecs后执行,后每隔interval循环执行一次
3). 信号集
有时候,往往一个进程会忽略或者屏蔽掉多个信号,这是如果我们一个个的输入会特别麻烦,因此,就有了信号集的概念!
信号集的数据结构如下:
File: /usr/include/bits/sigset.h
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
Linux提供了大量的信号集操作函数,具体如下:
①. 设置进程屏蔽信号集
- sigprocmask
作用:
设置当前进程屏蔽的信号集合头文件:
#include <signal.h>
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
参数:
how:
How Description SIG_BLOCK 将set信号集添加到当前进程的屏蔽信号集中 SIG_UNBLOCK 将set信号集从到当前进程的屏蔽信号集中删除 SIG_SETMASK 设置当前屏蔽的信号集为set set: 设置的信号集;若为NULL,则相当于查询当前信号集 oldset: 保存执行该函数前的屏蔽信号集
- 返回值:
成功:0
失败:-1
②. 获取当前未决信号
- sigpending:
作用:
获取当前进程的未决信号(即被屏蔽的尚未执行的信号)头文件:
#include <signal.h>
函数原型:
int sigpending(sigset_t *set)
参数:
set: 存储未决信号
返回值:
成功:0
失败:-1
③. 信号集操作
包括清空、填充信号集/添加、删除信号/判断信号集的成员
头文件:
#include <signal.h>
函数原型:
函数 原型 描述 sigemptyset int sigemptyset(sigset_t *set) 清空信号集 sigfillset int sigfillset(sigset_t *set) 置位信号集 sigaddset int sigaddset(sigset_t *set, int signum) 添加信号到信号集 sigdelset int sigdelset(sigset_t *set, int signum) 删除某个信号 sigismember int sigismember(const sigset_t *set, int signum) 判断信号是否为信号集中的成员 sigisemptyset int sigisemptyset(sigset_t *set) 判断信号集是否为空
- 返回值:
成功: 0
失败:-1
4). 等待信号
进程可以处于挂起状态而等待某些特定的信号,函数如下:
头文件:
#include <signal.h>
函数原型:
int pause(void) //暂停程序执行,等待任意信号的到来恢复工作 int sigsuspend(const sigset_t *mask) //暂停程序,mask为屏蔽信号集,等待除此之外的任意信号的到来;当有信号到来后,进程屏蔽集将恢复为原来的信号集(即执行该函数之前的信号集)
返回值:
总是返回 -1
5. 示例代码
有两个进程(非亲缘),分别为client
和server
:
- client:
- 通过task_name(即“server”)获取其PID;
- 通过pid,向“server”持续发送10次信号 “SIGUSR1”
- 结束自身前,发送“SIGUSR2”信号给“server”,结束“server”
- 进程退出
- server:
- 安装监听信号:SIGUSR1和SIGUSR2
- 当收到
SIGUSR1
时,打印信息- 当收到
SIGUSR2
时,结束本进程
本程序包含四个文件:
- server.c: 服务端
- client.c: 客户端
- task.c: 用于获取PID或task_name
- task.h:
1). server.c:
#include "./task.h"
void handler(int num)
{
static i=0;
i++;
printf("The %d times receive signal!\n", i);
}
int main(int argc, char* argv[])
{
int pid=-1;
int ret;
signal(SIGUSR1, handler);
signal(SIGUSR2, NULL);
while(1);
return 0;
}
2). client.c:
#include "./task.h"
#define TAR_TASK "server"
int main(int argc, char* argv[])
{
int pid=-1;
int ret;
//get the server task pid
ret = getPidByName(&pid, TAR_TASK);
if((-1 == ret) || (-1 == pid))
{
printf("getPidByName was failed!\n");
exit(EXIT_FAILURE);
}
printf("The task %s: pid=%d\n", TAR_TASK, pid);
//Send a signal to server every 1 second
int i=0;
while(i < 10)
{
kill(pid,SIGUSR1);
i++;
printf("send %d times signal!\n", i);
sleep(1);
}
printf("\nEnd the server and myself!\n");
kill(pid, SIGUSR2);
kill(getpid(), SIGSTOP);
return 0;
}
3). task.c:
#include "./task.h"
int getPidByName(pid_t *pid, char *task_name)
{
////1. open the /proc , and every read, it will return the next dir struct dirent
DIR * dirp;
dirp = opendir("/proc");
if(NULL == dirp)
{
perror("opendir");
return -1;
}
//2.read the directory
struct dirent *dp;
while(NULL != (dp = readdir(dirp)))
{
//if direactory is . or .. or not a direct, skip
if((dp->d_name == ".") || (dp->d_name == ".."))
continue;
if(DT_DIR != dp->d_type)
continue;
//open the /proc/pid/status file
char fileName[128];
FILE *fd ;
sprintf(fileName, "/proc/%s/status", dp->d_name);
fd = fopen(fileName, "r");
if(NULL != fd)
{
//copy the /proc/pid/status content to buf
char buf[128];
fgets(buf, sizeof(buf), fd);
//
char cur_task[32] ;
sscanf(buf, "%*s%s", cur_task);
if(!strcmp(cur_task, task_name))
{
sscanf(dp->d_name, "%d", pid);
}
}
}
return 0;
}
int getNameByPid(pid_t pid, char *task_name)
{
char fileName[128];
sprintf(fileName, "/proc/%d/status", pid);
FILE *fd;
fd = fopen(fileName, "r");
if(NULL == fd)
{
perror("fopen");
return -1;
}
char buf[128];
fgets(buf, sizeof(buf), fd);
sscanf(buf, "%*s%s", task_name);
return 0;
}
4). task.h:
#ifndef TASK_H
#define TASK_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <dirent.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/ipc.h>
extern int getPidByName(pid_t *pid, char *task_name);
extern int getNameByPid(pid_t pid, char *task_name);
#endif