2021-2022-1-diocs-信号和信号处理
20191205 2021-2022-1-diocs-信号和信号处理(第九周学习笔记)
一、任务详情
自学教材第5章,提交学习笔记(10分)
知识点归纳以及自己最有收获的内容 (3分)
问题与解决思路(2分)
实践内容与截图,代码链接(3分)
...(知识的结构化,知识的完整性等,提交markdown文档,使用openeuler系统等)(2分)
二、知识点总结
本章讲述了信号和信号处理;介绍了信号和中断的统一处理,有助于从正确的角度看待信号;将信号视为进程中断,将进程从正常执行转移到信号处理;解释了信号的来源,包括来自硬件、异常和其他进程的信号;然后举例说明了信号在Unix/Linux 中的常见用法;详细解释了Unix/Linux中的信号处理,包括信号类型、信号向量位、信号掩码位、进程PROC结构体中的信号处理程序以及信号处理步骤;用示例展示了如何安装信号捕捉器来处理程序异常,如用户模式下的段错误;还讨论了将信号用作进程间通信(IPC)机制的适用性。读者可借助该编程项目,使用信号和管道来实现用于进程交换信息的进程间通信机制。
思维导图
1.信号和中断
(1)首先,我们将进程的概念概括为:一个"进程"(引号中)就是一系列活动。广义的"进程"包括
● 从事日常事务的人。
●在用户模式或内核模式下运行的 Unix/Linux 进程。
● 执行机器指令的CPU。
(2)"中断"是发送给"进程"的事件,它将"进程"从正常活动转移到其他活动,称为"中断处理"。"进程"可在完成"中断"处理后恢复正常活动。
(3)"中断"一词可应用于任何"进程",并不仅限于计算机中的CPU。
1.人员中断:根据来源,中断可分为三类:
●来自硬件的中断∶大楼着火,闹钟响了等。
●来自其他人的中断∶电话响了,有人敲门等。
●自己造成的中断∶切到手指,吃得太多等。
按照紧急程度,中断可分为以下几类∶
●不可屏蔽(NMI)∶大楼着火!
●可屏蔽∶有人敲门等。
人员的每个动作函数都是通过本能或经验实现的。由于人员中断的种类太多,所以不能在上表中全部列出,但是思路应该清晰。
2.进程中断
这类中断是发送给进程的中断。当某进程正在执行时,可能会收到来自3个不同来源的中断:
●来自硬件的中断:终端、间隔定时器的"Ctrl+C"组合键等。
●来自其他进程的中断:kill(pid,SIG#)、death_of_child 等。
●自己造成的中断∶除以0、无效地址等。
1.这类中断是发送给处理器或CPU的信号。它们也有三个可能的来源:
●来自硬件的中断:定时器、I/O设备等。
●来自其他处理器的中断:FFP、DMA、多处理器系统中的其他 CPU。
●自己造成的中断:除以0、保护错误、INT指令。
2.进程的陷阱错误
进程可能会自己造成中断。这些中断是由被 CPU 识别为异常的错误引起的,例如除以0、无效地址、非法指令、越权等。当进程遇到异常时,它会陷入操作系统内核,将陷阱原因转换为信号编号,并将信号发送给自己。如果在用户模式下发生异常,则进程的默认操作是终止,并使用一个可选的内存转储进行调试。如果在内核模式下发生陷阱,原因一定是硬件错误,或者很可能是内核代码中的漏洞,在这种情况下,内核无法处理。
3.Unix/Linux中的信号处理
1.Unix/Linux支持31种不同的信号,每种信号在 signal.h文件中都有定义。
#define SIGHUP#define SIGINT
#define SIGQUIT
#define SIGILL #define SIGTRAP
#define SIGABRT #define SIGIOT
#define SIGBUS
#define SIGFPE
#define SIGKILL
#define SIGUSR1
#define SIGSEGV
#define SIGUSR2
#define SIGPIPE #define SIGALRM
#define SIGTERM
#define SIGSTKFLT
#define SIGCHLD
#define SIGCONT
#define SIGSTOP
#define SIGTSTP
#define SIGTTIN
#define SIGTTOU
#define SIGURG
#define SIGXCPU
#define SIGXFSZ
#define SIGVTALRM
#define SIGPROF
#define SIGWINCH
#define SIGPOLL
#define SIGPWR
#define SIGSYS
2.信号的来源
来自硬件中断的信号:在进程执行过程中,一些硬件中断被转换为信号发送给进程。
来自异常的信号:当用户模式下的进程遇到异常时,会陷入内核模式,生成一个信号,并发送给自己。常见的陷阱信号有SIGFPE(8),表示浮点异常(除以0),最常见也是最可怕的是SIGSEGV(11),表示段错误,等等。
来自其他进程的信号:进程可使用kil(pid,sig)系统调用向 pid标识的目标进程发送信号。读者可以尝试以下实验。
3.信号处理函数
每个进程PROC 都有一个信号处理数组 int sig[32]。Sig[32]数组的每个条目都指定了如何处理相应的信号,其中0表示 DEFault(默认).1表示 IGNore(忽略).其他非零值表示用户模式下预先安装的信号捕捉(处理)函数。下图给出了信号位向量、屏蔽位向量和信号处理函数。
3.信号处理步骤
(1)当某进程处于内核模式时,会检查信号并处理未完成的信号。如果某信号有用户安装的捕捉函数,该进程会先清除信号,获取捕捉函数地址,对于大多数陷阱信号,则将已安装的捕捉函数重置为 DEFault。然后,它会在用户模式下返回,以执行捕捉函数,以这种方式篡改返回路径。当捕捉函数结束时,它会返回到最初的中断点,即它最后进入内核模式的地方。
(2)重置用户安装的信号捕捉函数:用户安装的陷阱相关信号捕捉函数用于处理用户代码中的陷阱错误。由于捕捉函数也在用户模式下执行,因此可能会再次出现同样的错误。如果是这样,该进程最终会陷入无限循环,一直在用户模式和内核模式之间跳跃。为了防止这种情况,Unix 内核通常会在允许进程执行捕捉函数之前先将处理函数重置为 DEFault。这意味着用户安装的捕捉函数只对首次出现的信号有效。
(3)信号和唤醒:在Unix/Linux,内核中有两种 SLEEP进程;深度休眠进程和浅度休眠进程。前一种进程不可中断,而后一种进程可由信号中断。如果某进程处于不可中断的SLEEP 状态,到达的信号(必须来自硬件中断或其他进程)不会唤醒进程。如果它处于可中断的SLEEP状态,到达的信号将会唤醒它。
4.信号与异常
Unix信号最初设计用于以下用途
- 作为进程异常的统一处理方法;
- 让进城通过预先安装的信号捕捉函数用户模式下的程序错误;
- 在特殊情况下,它会让某一个进程通过信号杀死另一个进程。
5.Linux中的IPC
1.管道和FIFO
管道的主要用途是连接一对管道写进程和读进程。管道写进程可将数据写入管道,读进程可从管道中读取数据。管道控制机制要对管道读写操作进行同步控制。未命名管道供相关进程使用。命名管道是FIFO的,可供不相关进程使用。在 Linux中的管道读取操作为同步和阻塞。如果管道仍有写进程但没有数据,读进程会进行等待。
2.信号
进程可使用 kill 系统调用向其他进程发送信号,其他进程使用信号捕捉函数处理信号。将信号用作IPC的一个主要缺点是信号只是用作通知,不含任何信息内容。
3.线程同步机制
Linux 不区分进程和线程。在 Linux中,进程是共享某些公共资源的线程。如果是使用有共享地址空间的clone(系统调用创建的进程,它们可使用互斥量和条件变量通过共享内存进行同步通信。另外,常规进程可添加到共享内存,使它们可作为线程进行同步。
三、最有收获的内容
安装信号使用系统调用
(1)在执行已安装的信号捕捉函数之前,通常将信号处理函数重置为 DEFault。为捕捉下次出现的相同信号,必须重新安装捕捉函数。这可能会导致下一个信号和信号处理函数重新安装之间出现竞态条件。相反,sigaction()在执行当前捕提函数时会自动阻塞下一个信号,因此不会出现竞态条件。
(2)signal()不能阻塞其他信号。必要时,用户必须使用 sigprocmask()显式地阻塞或解锁其他信号。相反,sigaction()可以指定要阻塞的其他信号。
(3)signal()只能向捕捉函数发送一个信号编号。sigaction()可以传输关于信号的其他信息。
(4)signal(()可能不适用于多线程程序中的线程。sigaction()适用于线程。
(5)不同Unix版本的signalO)可能会有所不同。sigaction()采用的是POISX标准,可移植性更好。
示例:
sigaction()的使用
代码如下:
#include<stdio.h> #include<unistd.h> #include<signal.h> void handler(int sig,siginfo_t *siginfo,void *context) { printf("handler:sig=%d from PID=%d UID=%d\n",sig,siginfo->si_pid,siginfo->si_uid); } int main(int argc,char *argv[]) { struct sigaction act; memset(&act,0,sizeof(act)); act.sa_sigaction = &handler; act.sa_flags = SA_SIGINFO; sigaction(SIGTERM,&act, NULL); printf("proc PID=%d looping\n"); printf("enter Kill PID to send SIGTERM signal to it\n",getpid()); while(1){ sleep(10); } }
编译运行结果:
四、问题与解决思路(解决思路是基于OpenEuler系统下解决的)
问题:什么是中断和信号?中断的处理过程是如何进行的?
解决:
1.中断的基本概念
中断是指计算机在执行期间,系统内发生任何非寻常的或非预期的急需处理事件,使得CPU暂时中断当前正在执行的程序而转去执行相应的事件处理程序,待处理完毕后又返回原来被中断处继续执行或调度新的进程执行的过程。引起中断发生的事件被称为中断源。中断源向CPU发出的请求中断处理信号称为中断请求,而CPU收到中断请求后转到相应的事件处理程序称为中断响应。
2.中断处理过程
(1)CPU检查响应中断的条件是否满足。CPU响应中断的条件是:有来自于中断源的中断请求、CPU允许中断。
(2)如果CPU响应中断,则CPU关中断,使其进入不可再次响应中断的状态。
(3)保存被中断进程现场。
(4)分析中断原因,调用中断处理子程序。在多个中断请求同时发生时,处理优先级最高的中断源发出的中断请求。
(5)执行中断处理子程序。对陷阱来说,在有些系统中则是通过陷阱指令向当前执行进程发出软中断信号后调用对应的处理子程序执行。
(6)退出中断,恢复被中断进程的现场或调度新进程占据处理器。
(7)开中断,CPU继续执行。
3.信号及信号来源
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
4.查看Linux所支持的信号可用:kill -l,一共有64种,如图所示:
五、实践内容(截图、代码链接)
1.编写C代码使用信号捕捉函数和long jump来绕过导致段错误的程序代码,使程序继续执行或正常终止
C语言程序中最常见的段错误的原因是解除空指针或无效指针关联、数组越界等。当某进程遇到无效内存异常时,它会陷人操作系统内核,生成SIGSEGV(1l)信号,并发送给自己。SIGSEGV信号的默认处理函数是0,将导致进程终止。如果进程忽略该信号,它会再次返回同一错误指令,导致无限循环。若用户已经安装了SIGSEGV信号的捕捉函数,进程会执行信号捕捉函数,但是在执行结束后仍会返回同一错误指令。在任何段错误情况下,进程的唯—选择似平只能是异常终止。
代码如下:
代码链接:
https://gitee.com/two_thousand_and_thirteen/codes/6zvpxe0sg29iltrh3oub422
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<signal.h> #include<setjmp.h> jmp_buf env; int count = 0; void handler(int sig,siginfo_t *siginfo,void *context) { printf("handler:sig=&d from PID=%d UID=%d count=%d\n",sig,siginfo->si_pid,siginfo->si_uid,++count); if (count>=4) longjmp(env,1234); } int BAD() { int *ip=0; printf("in BAD():try to dereference NULL pointer\n"); *ip=123; printf("should not see this line\n"); } int main(int argc,char *argv[]) { int r; struct sigaction act; memset (&act,0,sizeof(act)); act.sa_sigaction = &handler; act.sa_flags=SA_SIGINFO; sigaction(SIGSEGV, &act,NULL); if((r=setjmp(env))==0) BAD(); else printf("proc %d survived SEGMENTATION FAULT:r=%d\n",getpid(),r); printf("proc %d looping\n"); while(1); }
编译运行结果:
2.在Linux下编写C语言实现一个消息的IPC
在基本代码中,父进程可创建管道,复刻子进程,并充当管道的写进程。子进程是管道的读进程。它会安装一个 SIGUSR1信号捕捉函数,并进行循环。当父进程运行时,会获取一个输入字符串。将该字符串作为一条消息写入管道。向子进程发送一个SIGUSR1信号,在信号捕捉函数中接收和显示消息。然后,它会重复刚才的循环。
代码如下:
代码链接:
https://gitee.com/two_thousand_and_thirteen/codes/1cnia7y4tmspowv389h6r13
#include<stdio.h> #include<signal.h> #include<string.h> #define LEN 64 int ppipe[2]; int pid; char line[LEN]; int parent() { printf("parent %d running\n",getpid()); close(ppipe[0]); while(1){ printf("parent %d: input a line : \n",getpid()); fgets(line,LEN,stdin); line[strlen(line)-1]=0; printf("parent %d write to pipe\n",getpid()); write(ppipe[1],line,LEN); printf("parent %d send signal 10 to %d\n",getpid(),pid); kill(pid,SIGUSR1); } } void chandler(int sig) { printf("\nchild %d got an interrupt sig=%d\n",getpid(),sig); read(ppipe[0],line,LEN); printf("child %d get a message = %s\n",getpid(),line); } int child() { char msg[LEN]; int parent = getppid(); printf("child %d running\n",getpid()); close(ppipe[1]); signal(SIGUSR1,chandler); while(1); } int main() { pipe(ppipe); pid=fork(); if(pid) parent(); else child(); }
编译运行结果: