• Linux系统编程14_信号和进程状态


    ====================信号介绍=========================
    SIGKILL 9 exit信号,是不会被阻塞的,不能被忽略;杀死进程的终极办法
    SIGSTOP 停止,并不消灭进程

    SIGINT 2  Ctrl + C时OS送给前台进程组中每个进程, 默认处理的时候会调用exit终止程序
    SIGQUIT 3 默认处理动作是终止进程并且Core Dump
    SIGABRT 6 调用abort函数,进程异常终止

    SIGUSR1 10 用户自定义信号
    SIGSEGV 11 无效存储访问时OS发出该信号,段错误
    SIGUSR2 12 用户自定义信号
    SIGPIPE 13 涉及管道和socket
    SIGALARM 14 涉及alarm函数的实现
    SIGTERM 15 多半会被阻塞,kill命令发送的OS默认终止信号
    SIGCHLD 17 子进程终止或者是停止时,OS向其父进程发送此信号
    =====================进程状态=====================
    R 可执行状态 在CPU上运行
    S 可中断的睡眠状态(TASK_INTERRUPTIBLE) 被挂起 可随时被唤醒
    D 不可中断的睡眠状态(TASK_UNINTERRUPTIBLE) 不会相应异步信号 kill -9都杀不死
    T 暂停状态或跟踪状态(TASK_STOPPED or TASK_TRACED)
    X 退出状态(TASK_DEAD - EXIT_DEAD),进程即将被销毁
    Z 退出状态 (TASK_DEAD - EXIT_ZOMBIE),进程成为僵尸进程

    TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。
    如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。
    在进程对某些硬件进行操作时,可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。
    这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。
    linux系统中也存在容易捕捉的TASK_UNINTERRUPTIBLE状态。执行vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用exit或exec。


    向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态
    SIGSTOP与SIGKILL信号一样,是非常强制的。
    不允许用户进程通过signal系列的系统调用重新设置对应的信号处理函数。
    向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态。

    当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。
    “正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。
    比如在gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。
    而在其他时候,被跟踪的进程还是处于前面提到的那些状态。
    对于进程本身来说,TASK_STOPPED和TASK_TRACED状态很类似,都是表示进程暂停下来。
    而TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒。
    只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),
    或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。


    EXIT_ZOMBIE
    在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。
    之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。
    而其父进程很可能会关心这些信息。比如在shell中,$?变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为if语句的判断条件。
    当然,内核也可以将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。
    但是使用task_struct结构更为方便,因为在内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。
    释放掉task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。
    父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。
    然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。
    子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。
    这个信号默认是SIGCHLD,但是在通过clone系统调用创建子进程时,可以设置这个信号。


    EXIT_DEAD
    而进程在退出过程中也可能不会保留它的task_struct。
    比如这个进程是多线程程序中被detach过的进程(进程?线程?参见《linux线程浅析》)。
    或者父进程通过设置SIGCHLD信号的handler为SIG_IGN,显式的忽略了SIGCHLD信号。
    (这是posix的规定,尽管子进程的退出信号可以被设置为SIGCHLD以外的其他信号。)
    此时,进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码立即就会将该进程彻底释放。
    所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。
    ======================================================
    1号进程,pid为1的进程,又称init进程。
    inux系统启动后,第一个被创建的用户态进程就是init进程。它有两项使命
    1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);
    2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作;
    init进程不会被暂停、也不会被杀死(这是由内核来保证的)。
    它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态,“收尸”过程中则处于TASK_RUNNING状态。


    进程的初始状态
    进程是通过fork系列的系统调用(fork、clone、vfork)来创建的,内核(或内核模块)也可以通过kernel_thread函数创建内核进程。
    这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份,得到子进程。(可以通过选项参数来决定各种资源是共享、还是私有。)
    那么既然调用进程处于TASK_RUNNING状态(否则,它若不是正在运行,又怎么进行调用?),则子进程默认也处于TASK_RUNNING状态。
    另外,在系统调用调用clone和内核函数kernel_thread也接受CLONE_STOPPED选项,从而将子进程的初始状态置为 TASK_STOPPED。


    进程状态变迁
    进程自创建以后,状态可能发生一系列的变化,直到进程退出。
    而尽管进程状态有好几种,但是进程状态的变迁却只有两个方向——从TASK_RUNNING状态变为非TASK_RUNNING状态、或者从非TASK_RUNNING状态变为TASK_RUNNING状态。
    也就是说,如果给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNING状态),然后再响应SIGKILL信号而退出(变为TASK_DEAD状态)。
    并不会从TASK_INTERRUPTIBLE状态直接退出。
    进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。
    执行唤醒的进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中。于是被唤醒的进程将有机会被调度执行。

    而进程从TASK_RUNNING状态变为非TASK_RUNNING状态,则有两种途径:
    1、响应信号而进入TASK_STOPED状态、或TASK_DEAD状态;
    2、执行系统调用主动进入TASK_INTERRUPTIBLE状态(如nanosleep系统调用)、或TASK_DEAD状态(如exit系统调用);
    或由于执行系统调用需要的资源得不到满足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。
    显然,这两种情况都只能发生在进程正在CPU上执行的情况下。
    ======================================================
    kill -9 id //强制杀死进程,相当于发送了一个SIGKILL信号
    kill id //普通杀死进程,发送了SIGTERM信号
    ctrl +c //相当于发送了2号信号SIGINT,相当于中止当前进程;Ctrl + C时OS送给前台进程组中每个进程
    ctrl + z //相当于发送20号信号SIGTSTP,暂停/停止当前进程。
    kill -l //查看所有信号
    man 7 signal //查看信号用法
    kill -num(信号编号) pid //可以给指定进程发送信号
    ======================================================
    信号产生的原因:
    1、在终端按下某些组合键,终端驱动程序会发送信号给前台进程;
    2、硬件异常产生信号,由硬件检测并通知内核,然后内核向当前进程发送适当的信号;
    3、一个进程调用kill函数可以发送信号给另一个进程;
    ======================================================
    信号的处理有三种方法,分别是:忽略、捕捉和默认动作
    SIGSTOP和SIGKILL 不能被忽略;
    ======================================================
    信号相关函数:
    1、signal //基础版,信号处理注册函数
    typedef void (*sighandler_t)(int); //信号处理函数原型
    sighandler_t signal(int signum, sighandler_t handler); //信号处理注册函数

    2、sigaction //高级版,信号处理注册函数
    高级版可以携带一些数据;
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    //第一个参数signum应该就是注册的信号的编号;
    //第二个参数act如果不为空说明需要对该信号有新的配置;
    //第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

    struct sigaction {
    void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
    void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
    sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
    int sa_flags;//影响信号的行为,SA_SIGINFO表示能够接受数据
    };
    //回调函数句柄sa_handler、sa_sigaction只能任选其一


    3、kill //基础版 信号发送
    int kill(pid_t pid, int sig);


    4、sigqueue //高级版 信号发送
    int sigqueue(pid_t pid, int sig, const union sigval value);
    union sigval {
    int sival_int;
    void *sival_ptr;
    };
    //sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
    //sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队
    ======================================================
    可靠信号和不可靠信号
    不可靠信号:信号可能丢失,一旦信号丢失了,进程并不能知道信号丢失;
    可靠信号:阻塞信号,
    信号编号小于等于31的信号都是不可靠信号,之后的信号是可靠信号。

    可靠信号对应两个函数,sigqueue,sigaction;信号发送和处理;
    可靠信号克服了信号可能丢失的问题;
    这些可靠信号支持排队,不会丢失;

    可靠信号的阻塞和未决:
    如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,
    当进程解除对待处理信号的阻塞时,待处理信号就会立刻被处理。
    ======================================================
    信号集函数 sigemptyset()、sigprocmask()、sigpending()、sigsuspend():
    https://www.cnblogs.com/52php/p/5815125.html

    ============【信号的概念】============================
    信号是软中断,它提供了一种处理异步事件的方法。

    首先,每个信号都有一个名字。这些名字都以三个字符SIG开头。

    在头文件<signal.h>中,这些信号都被定义为正整数(信号编号)。

    实际上,实现将各信号定义在另一个头文件中,但是该头文件又包括在<signal.h>中。
    通常,若应用程序和内核两者都需使用同一定义,那么就将有关信息放置在内核头文件中,然后用户级头文件再包括该内核头文件。

    不存在编号为0的信号。(kill函数对信号编号0有特殊的应用。POSIX.1将此种信号编号值称为空信号。)

    产生信号的条件:
    1)当用户按某些终端键时,引发终端产生的信号。
    2)硬件异常产生信号。
    3)进程调用kill(2)函数可将信号发送给另一个进程或进程组。
    (自然,对此有所限制:接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者必须是超级用户。)
    4)用户可用kill(1)命令将信号发送给其他进程。
    当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。(这里指的不是硬件产生的条件,而是软件条件。)

    信号是异步事件的经典实例。
    产生信号的事件对进程而言是随机出现的。
    进程不能简单地测试一个变量(例如errno)来判别是否出现了一个信号,
    而是必须告诉内核“在此信号出现时,请执行下列操作”。

    可以要求内核在某个信号出现时按照下列三种方式之一进行处理,我们称之为信号的处理或者与信号相关的动作。
    (1)忽略此信号。大多数信号都可使用这种方法进行处理,但是有两种信号决不能被忽略:SIGKILL和SIGSTOP。
    这两种信号不能被忽略的原因是:它们向超级用户提供了使进程终止或停止的可靠方法。
    另外,如果忽略某些由硬件异常产生的信号(例如除以0),则进程的运行行为是未定义的。
    (2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时调用一个用户函数。
    在用户函数中,可执行用户希望对这种事件进行的处理。注意,不能捕捉SIGKILL和SIGSTOP信号。
    (3)执行系统默认动作。注意,针对大多数信号的系统默认动作是终止进程。

    表10-1列出了所有信号的名字,说明了哪些系统支持此信号以及针对这些信号的系统默认动作。在“默认动作”列中,
    “终止+core”表示在进程当前工作目录的core文件中复制该进程的存储映像。大多数UNIX调试程序都使用core文件以检查进程终止时的状态。

     

    用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。
    如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

    软中断信号(signal,简称为信号)用来通知进程发生了异步事件。
    进程之间可以互相通过系统调用kill发送软中断信号

    内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

    信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。


    用户在终端按下某些组合键时,终端驱动程序会发送信号给前台进程。
    硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
    一个进程调用kill(2)函数可以发送信号给另一个进程。


    信号就是一个定义在signal.h文件中的一个正整数常量;
    使用kill -l可以查看系统中定义的信号列表。
    使用man 7 signal可以查看信号的详细说明;
    编号1~31是普通信号,34以上是实时信号;

    信号的发送者有很多,比如终端驱动程序,进程,系统。而接收者大多是一个进程。
    那么怎么做就是给某进程发送一个信号呢?事实上,给进程发一个信号就是修改目标进程pcb结构体中的关于信号的字段(让进程记录此信号)。
    进程是否接收到信号本身是一个原子问题。它要么收到,要么没收到。所以可以用位图来表示进程是否收到信号,只需要修改一个比特位(操作系统完成):收到信号就置1。

    进程收到信号后,其可选的处理动作有以下三种:
    忽略此信号。
    执行该信号的默认处理动作(终止该信号)。
    提供一个信号处理函数(自定义动作),要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

    信号之signal函数
    #include <signal.h>
    void (*signal(int signo, void (*func)(int)))(int);

    signo参数是信号名(参见:http://www.cnblogs.com/nufangrensheng/p/3514157.html中UNIX系统信号Signal栏下的信号名)。func的值是常量SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数的地址。
    如果指定SIG_IGN,则向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略)。
    如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为“捕捉”该信号。
    称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。

    linux信号基本概念及如何产生信号:https://www.cnblogs.com/LiuYanYGZ/p/9567092.html

    ctrl-c 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序。
    ctrl-z 发送 SIGTSTP 信号给前台进程组中的所有进程,常用于挂起一个进程。
    ctrl-d 不是发送信号,而是表示一个特殊的二进制值,表示 EOF。
    ctrl- 发送 SIGQUIT 信号给前台进程组中的所有进程,终止前台进程并生成 core 文件。

    所以SIGINT的默认处理动作是终止进程,而SIGQUIT的默认处理动作是终止进程并且Core Dump。
    核心转储core dumped
    当个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
    也叫核心转储,帮助开发者进行调试,在程序崩溃时把内存数据dump到硬盘上,让gdb识别。

     

    所以有+表示前台进程,无+表示后台进程。
    Ctrl-C产生的信号只能发给前台进程。因为后台进程使Shell不必等待进程结束就可以接受新的命令,启动新的进程。

    而前台进程运行时占用SHELL,它运行的时候SHELL不能接受其他命令。

    shell自动将后台进程中对中断和退出信号的处理方式设置为忽略。

    于是,当按中断键时就不会影响到后台进程。如果没有执行这样的处理,那么当按中断键时,它不但会终止前台进程,还会终止所有后台进程。

    知行合一
  • 相关阅读:
    MySQL 获得当前日期时间 函数
    Jquery 将表单序列化为Json对象
    Eclipse远程调试(远程服务器端监听)
    使用Eclipse进行远程调控
    Java基础教程(3)--回顾HelloWorld
    Java基础教程(2)--Java开发环境
    Java基础教程(1)--概述
    4.9上机
    4.2上机
    第四周作业
  • 原文地址:https://www.cnblogs.com/grooovvve/p/14651121.html
Copyright © 2020-2023  润新知