一:信号的基本介绍
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式(进程在运行过程中,随时可能被各种信号打断)。
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了那些系统事件。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进
程恢复执行再传递个它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞取消时才被传递给进程。
二:信号的产生
A.用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如ctr+c产生SIGINT, ctr + \产生SIGQUI信号,ctr + z产生SIGTSTP。
B.硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给当前进程 。我们常见的段错误。
C.一个进程调用int kill(pid_t pid,int sig)函数可以给另一个进程发送信号。
D.可以用kill命令给某个进程发送信号,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。
E.当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生
SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。
三:linux操作系统支持的信号
A. kill -l命令查看当前系统支持的所有的信号
B:常用信号的含义
信号名 | 含义 | 默认操作 |
SIGHUP |
该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联。 |
终止 |
SIGINT |
该信号在用户键入INTR字符(通常是Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程。 | 终止 |
SIGQUIT |
该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-\)来控制。 | 终止 |
SIGILL |
该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发 出。 |
终止 |
SIGFPE |
该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 |
终止 |
信号名 | 含义 | 默认操作 |
SIGKILL |
该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略。 | 终止 |
SIGALRM | 该信号当一个定时器到时的时候发出。 | 终止 |
SIGSTOP | 该信号用于暂停一个进程,且不能被阻塞、处理或忽略。 | 暂停进程 |
SIGTSTP |
该信号用于暂停交互进程,用户可键入SUSP字符(通常是Ctrl-Z)发出这个信号。 | 暂停进程 |
SIGCHLD | 子进程改变状态时,父进程会收到这个信号 | 忽略 |
SIGABORT | 该信号用于结束进程 | 终止 |
四:linux中进程对信号处理
忽略信号,即对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及
SIGSTOP。
捕捉信号,定义并注册信号处理函数,当信号发生时,执行相应的处理函数。
【重点】。
执行缺省操作,Linux对每种信号都规定了默认操作
五:相关API
1:信号的发送(kill和raise)
#include <sys/types.h>
#include <signal.h>
函数原型:int kill(pid_t pid, int sig); 函数功能:给进程 id 为 pid 的进程发送信号
函数参数:@param pid : 发送信号的目标进程的 id
@param sig : 发送的信号编号,例如:9(SIGKILL) 返回值:成功调用返回 0 ,失败返回 -1 ,并设置 errno
#include <signal.h>
函数原型:int raise(int sig); 函数功能:给当前进程自己发送信号
函数参数:@param sig : 发送的信号编号
返回值:成功调用返回 0 ,失败返回 -1 ,并设置 errno
.*练习
我们通过终端kill -9 某个进程终止过一个进程,现在我们使用kill函数来终止一下。过程:
父进程创建一个子进程,父进程拿到子进程的进程ID;子进程中while循环打印hello,sleep(1);
父进程sleep(5)之后,给子进程发送9这个信号来终止子进程。
2:信号的捕捉(signal)
知识点回顾
void func(int);//函数的声明
void (*func)(int);//定义函数指针,指向void (int)类型的函数typedef int a;//给int类型的a起别名
typedef void (*funcp)(int);//给类型为void (*)(int);的函数指针起别名funcp 捕捉信号的处理过程:
#include <signal.h>
typedef void (*sighandler_t)(int);//指向函数的指针,表示信号处理函数的形式sighandler_t signal(int signum, sighandler_t handler);
函数功能 : 将信号与信号处理函数进行关联函数参数:@param signum : 信号的编号
@param handler : 信号处理函数的指针SIG_DFL : 表示默认操作
SIG_IGN : 表示忽略信号(SIGKILL和SIGSTOP时不能被忽略的) 返回值:成功调用返回信号处理函数的指针,否则,返回SIG_ERR
注意:sighandler_t handler中的int保存的是调用这个函数是因为哪个信号触发的,带过来对应的信号值。
.* 练习
(1)忽略ctrl+c对进程的终止信号。signal(SIGINT, SIG_IGN);
(2)在信号处理函数中将对应的信号的描述信息进行打印。
.* 练习:
fork前采用signal信号处理函数不阻塞,不轮询的方式回收僵尸态子进程[waitpid()函数]。 在信号处理函数signal_handler()中对信号进行收尸操作。然后利用fork函数创建一个子进程。休眠10s后退出。父进程是一个死循环,每秒输出"father do something…"的字符串。
提示:
子进程在终止时会给父进程发SIGCHLD,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数。我们这里调用waitpid非阻塞的回收僵尸态子进程。这样父进程只需要专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用waitpid函数清理子进程即可。
一般信号对僵尸态子进程的处理方法:
<1>父进程采用signal(SIGCHLD, hand_signal),采用信号处理函数,对接收到的SIGCHLD进行进行处理。在接收到SIGCHLD信号的时候,采用waitpid利用非阻塞的方式的释放它们的资源。若是使用wait()函数的话,父进程会阻塞。 [推荐使用]
<2>父进程采用signal(SIGCHLD, SIG_IGN),忽略SIGCHLD信号,这样子进程结束后,就不需要父进程来wait和释放资源。它会自动被过继给老祖宗init进程,int进程会负责释放他的资源,这样就不会产生僵尸态子进程。
3:定时闹钟函数(alarm)
unsigned int alarm(unsigned int seconds);
函数功能:给进程启动一个定时器,经过seconds秒后把SIGALRM信号发送给当前进程。函数参数:@seconds 秒
返回值:成功返回0,失败返回 -1
注意:一个进程只能有一个闹钟事件,若是多次使用alarm函数,则闹钟时间被刷新。
.*练习
(1)main函数中设置2s定时器,然后注册SIGALRM信号的处理函数,处理函数中打印当前时间到屏幕上
(2)我们现实中经常有这样的需求,需要每隔2s执行某个函数,这样怎么处理呢? 答案:在定时器处理函数里边,再次刷新闹钟alarm(2);
#include <stdio.h>
#include <time.h>
#include <signal.h>
void handler(int sig)
{
time_t tim = time(NULL);
struct tm *ptm = localtime(&tim);
printf("d-02d-02d 02d:02d:02d\n", ptm->tm_year+1900,
ptm->tm_mon+1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
alarm(1);
return;
}
int main(int argc, const char *argv[])
{
signal(SIGALRM, handler); alarm(1);
int n = 0; while(1)
{
printf("n = d\n", ++n); usleep(200);
}
return 0;
}
4:信号的等待(pause)
int pause(void);
特点:挂起一个进程,直到进程收到一个信号,进程会继续执行
上边的练习,在while循环中pause()一下。现象:不加pause()之前printf("n = d\n",
++n);会200ms打印一次,然后1秒打印一次时间,加上pause()之后,现象则是1s打印一次printf("n = d\n", ++n),时间也是1s一次。因为pause()会将进程挂起,接收到信号之后会继续,进程1s接收一次ALARM信号,则进程会1s会被唤醒一次。
time_t tim = time(NULL);
struct tm *ptm = localtime(&tim);
printf("d-02d-02d 02d:02d:02d\n", ptm->tm_year+1900, ptm->tm_mon+1, ptm->tm_mday, ptm->tm_hour,
ptm->tm_min, ptm->tm_sec); alarm(1);
return;
}
int main(int argc, const char *argv[])
{
signal(SIGALRM, handler); alarm(1);
int n = 0; while(1)
{
pause();
printf("n = d\n", ++n); usleep(200);
}
return 0;
}