码迷,mamicode.com
首页 > 系统相关 > 详细

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

时间:2021-04-13 12:31:37      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:ptr   操作   识别   注意   sele   get   信号signal   占用   出现   

====================信号介绍=========================
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自动将后台进程中对中断和退出信号的处理方式设置为忽略。

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

 

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

标签:ptr   操作   识别   注意   sele   get   信号signal   占用   出现   

原文地址:https://www.cnblogs.com/grooovvve/p/14651121.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!