• 信息安全系统设计与实现:第六章学习笔记


    信息安全系统设计与实现:第六章学习笔记

    20191331 lyx

    教材学习内容总结

    第六章 信号与信号处理

    学习目标

    通过本章的学习,了解信号和中断的统一处理,并从正确的角度看待信号,理解信号的进程中断的关系,了解信号的来源。理解类Unix系统中的信号处理,并在类Unix系统下进行相关实践。

    信号和中断

    中断,英文名为Interrupt,计算机的世界里处处都有中断,任何工作都离不开中断,可以说整个计算机系统就是由中断来驱动的。

    “中断”是从I/O设备或协处理器发送到CPU的外部请求,他将CPU从正常执行转移到中断处理。

    简单总结:中断即CPU停下当前的工作任务,去处理其他事情,处理完后回来继续执行刚才的任务

    • 首先,我们将进程的概念概括为:一个"进程"(引号中)就是一系列活动。广义的"进程"包括
      • 从事日常事务的人。
      • 在用户模式或内核模式下运行的 Unix/Linux 进程
      • 执行机器指令的CPU。

    根据来源 中断可分为三类

    人员中断:
    来自硬件的中断:大楼着火,闹钟响了等
    来自其他人的中断:电话响了,有人敲门等。
    自己造成的中断:切到手指,吃得太多等。
    
        按照紧急程度,中断可分为以下几类:
        不可屏蔽(NMI):大楼着火!
        可屏蔽:有人敲门等。
    
    进程中断
    这类中断是发送给进程的中断。当某进程正在执行时,可能会收到来自3个不同来源的中断:
    来自硬件的中断:终端、间隔定时器的“Ctrl+C”组合键等。
    来自其他进程的中断:kill(pid,SIG#), death_of_child等。
    自己造成的中断:除以0、无效地址等。
    每个进程中断都被转换为一个唯一ID号,发送给进程。与多种类的人员中断不同,我们始终可限制在一个进程中的中断的数量。Unix/Linux中的进程中断称为信号,编号为1到31。进程的PROC结构体中有对应每个信号的动作函数,进程可在收到信号后执行该动作函数。与人员类似,进程也可屏蔽某些类型的信号,以推迟处理。必要时,进程还可能会修改信号动作函数。
    
    硬件中断
    这类中断是发送给处理器或CPU的信号。它们也有三个可能的来源:
    来自硬件的中断:定时器、1/O设备等
    来自其他处理器的中断:FFP、DMA、多处理器系统中的其他CPU
    自己造成的中断:除以О、保护错误、INT指令。
    每个中断都有唯一的中断向量号。动作函数是中断向量表中的中断处理程序。
    
    进程的陷阱错误
    进程可能会自己造成中断。这些中断是由被CPU识别为异常的错误引起的,例如除以0、无效地址、非法指令、越权等。
    
    

    Unix/Linux信号示例

    • 按“Ctrl+C”组合键通常会导致当前运行的进程终止。原因如下:

      “Ctr1+C”组合键会生成一个键盘硬件中断。键盘中断处理程序将“Ctrl+C”组合键转换为SIGINT(2)信号,发送给终端上的所有进程,并唤醒等待键盘输人的进程。

    • 用户可使用nohup a.out &命令在后台运行一个程序。即使在用户退出后,进程仍将继续运行。nobup命令会使sh像往常一样复刻子进程来执行程序,但是子进程会忽略SIGHuP(1)信号。当用户退出时,sh会向与终端有关的所有进程发送一个SIGHUP信号。

    实例:使用 nohup 命令 将一个jar包在后台运行,即使关闭中断仍不会导致进程死亡。

    • 用户可以使用sh命令kill pid (orkill-s9pia) 杀死该程。

      方法如下。执行杀死的进程向pid标识的目标进程发送一个SIGTERM ( 15 )信号,请求它死亡。目标进程将会遵从请求并终止。

    Unix/Linux中的信号处理

    • Unix/Linux支持31种不同的信号,每种信号在 signal.h文件中都有定义。

    (下图例为 OpenEuler 20.03 LTS 操作系统)

    使用kill -l命令可以列出该系统所支持的所有信号的列表:

    信号值在32 之前的则有不同的名称,而信号值在32 以后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类..型的信号。前者是从UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。

    信号的来源

    • 来自硬件中断的信号
    • 来自异常的信号
    • 来自其他进程的信号

    信号处理函数&信号捕捉函数

    每个进程PROC 都有一个信号处理数组 int sig[32]。Sig[32]数组的每个条目都指定了如何处理相应的信号,其中0表示 DEFault(默认).1表示 IGNore(忽略).其他非零值表示用户模式下预先安装的信号捕捉(处理)函数。

    • 使用signal()函数处理时,只需要指出要处理的信号和处理函数即可。它主要是用于前32种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。

    Linux还支持一个更健壮、更新的信号处理函数sigaction(),推荐使用该函数。

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    

    第一个参数signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。  

    第二个参数handler:描述了与信号关联的动作,它可以取以下三种值:

    • SIG_IGN:忽略该信号
    • SIG_DFL:恢复对信号的系统默认处理
    • sighandler_t类型的函数指针:用户自定义的处理函数。

    在 实践环境下查看 sigaction 使用方法

    • 信号集函数组

    我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程。那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?信号集函数就是帮助我们解决这些问题的。

    int sigemptyset(sigset_t *set);
    //该函数的作用是将信号集初始化为空。
    
    int sigfillset(sigset_t *set);
    //该函数的作用是把信号集初始化包含所有已定义的信号。
    
    int sigaddset(sigset_t *set, int signo);
    //该函数的作用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。
    
    int sigdelset(sigset_t *set, int signo);
    //该函数的作用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1.
    
    int sigismember(sigset_t *set, int signo);
    //该函数的作用是判断给定的信号signo是否是信号集中的一个成员,如果是返回1,如果不是,返回0,如果给定的信号无效,返回-1;
    
    int sigpromask(int how, const sigset_t *set, sigset_t *oset);
    //该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。
    
    int sigpending(sigset_t *set);
    //该函数的作用是将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,成功调用返回0,否则返回-1,并设置errno表明错误原因。
    
    int sigsuspend(const sigset_t *sigmask);
    //该函数通过将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起进程的执行。注意操作的先后顺序,是先替换再挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到信号终止了程序,sigsuspend()就不会返回,如果接收到的信号没有终止程序,sigsuspend()就返回-1,并将errno设置为EINTR。
    

    sigaction实践

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    #include <string.h>
    
    void handler(int sig,siginfo_t *siginfo,void *context){
    printf("handler:sig=%d from PID=%d UID=%d
    ",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
    ");
    printf("enter Kill PID to send SIGTERM signal to it
    ",getpid());
    while(1){
    sleep(10);
    }
    }
    
    

    运行结果:

    信号处理步骤

    • 当某进程处于内核模式时,会检查信号并处理未完成的信号。如果某信号有用户安装的捕捉函数,该进程会先清除信号,获取捕捉函数地址,对于大多数陷阱信号,则将已安装的捕捉函数重置为 DEFault。然后,它会在用户模式下返回,以执行捕捉函数,以这种方式篡改返回路径。当捕捉函数结束时,它会返回到最初的中断点,即它最后进入内核模式的地方。

    • 重置用户安装的信号捕捉函数:用户安装的陷阱相关信号捕捉函数用于处理用户代码中的陷阱错误。由于捕捉函数也在用户模式下执行,因此可能会再次出现同样的错误。如果是这样,该进程最终会陷入无限循环,一直在用户模式和内核模式之间跳跃。为了防止这种情况,Unix 内核通常会在允许进程执行捕捉函数之前先将处理函数重置为 DEFault。这意味着用户安装的捕捉函数只对首次出现的信号有效。

    • 信号和唤醒:在Unix/Linux,内核中有两种 SLEEP进程;深度休眠进程和浅度休眠进程。前一种进程不可中断,而后一种进程可由信号中断。如果某进程处于不可中断的SLEEP 状态,到达的信号(必须来自硬件中断或其他进程)不会唤醒进程。如果它处于可中断的SLEEP状态,到达的信号将会唤醒它。

    信号与异常

    Unix信号最初设计用于以下用途:

    • 作为进程异常的统一处理方法;
    • 让进城通过预先安装的信号捕捉函数用户模式下的程序错误;
    • 在特殊情况下,它会让某一个进程通过信号杀死另一个进程。

    信号用做IPC

    实践:段错误捕捉函数

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <string.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
    ",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
    ");
    *ip=123;
    printf("should not see this line
    ");
    }
    
    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
    ",getpid(),r);
    printf("proc %d looping
    ");
    while(1);
    }
    

    运行结果:

    管道和FIFO

    管道的主要用途是连接一对管道写进程和读进程。管道写进程可将数据写入管道,读进程可从管道中读取数据。管道控制机制要对管道读写操作进行同步控制。未命名管道供相关进程使用。

    在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操作。

    信号

    进程可使用 kill 系统调用向其他进程发送信号,其他进程使用信号捕捉函数处理信号。将信号用作IPC的一个主要缺点是信号只是用作通知,不含任何信息内容

    线程同步机制

    Linux 不区分进程和线程。在 Linux中,进程是共享某些公共资源的线程。如果是使用有共享地址空间的clone(系统调用创建的进程,它们可使用互斥量和条件变量通过共享内存进行同步通信。另外,常规进程可添加到共享内存,使它们可作为线程进行同步。

    编程实践:实现一个消息IPC

    #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
    ",getpid());
    close(ppipe[0]);
    while(1){
    printf("parent %d: input a line : 
    ",getpid());
    fgets(line,LEN,stdin);
    line[strlen(line)-1]=0;
    printf("parent %d write to pipe
    ",getpid());
    write(ppipe[1],line,LEN);
    printf("parent %d send signal 10 to %d
    ",getpid(),pid);
    kill(pid,SIGUSR1);
    }
    }
    
    void chandler(int sig){
    printf("
    child %d got an interrupt sig=%d
    ",getpid(),sig);
    read(ppipe[0],line,LEN);
    printf("child %d get a message = %s
    ",getpid(),line);
    }
    
    int child(){
    char msg[LEN];
    int parent = getppid();
    printf("child %d running
    ",getpid());
    close(ppipe[1]);
    signal(SIGUSR1,chandler);
    while(1);
    }
    
    int main(){
    pipe(ppipe);
    pid=fork();
    if(pid)
    parent();
    else
    child();
    }
    

    运行结果:

    参考资料

    一文讲透计算机的“中断” https://zhuanlan.zhihu.com/p/360548214

  • 相关阅读:
    jstl格式化日期
    linux sqlplus查询数据中文乱码解决方法记录
    MyBatis insert操作插入,返回主键from官方
    Debian apt-get 用法
    Java并发编程--Semaphore
    Java并发编程--CyclicBarrier
    Java并发编程--CountDownLatch
    Java并发编程--BlockingQueue
    Java并发编程--ReentrantReadWriteLock
    Java并发编程--Lock
  • 原文地址:https://www.cnblogs.com/DKYcaiji/p/15528175.html
Copyright © 2020-2023  润新知