一、进程
程序和进程
程序是二进制文件,静态存储在磁盘上,不会占用内存和CPU资源;程序运行之后就会产生进程,进程是动态的,会占用一定的内存和CPU资源。
一颗物理cpu在同一时刻只能运行一个进程,多颗物理cpu才能真正意义上实现多任务。人们之所以会产生系统能够同时运行多个进程的“错觉”,是因为CPU在极短的时间内进行进程间切换实现的。
在Linux上,系统通过调度器来决定下一个要执行的进程。进程何时运行, 取决于它的优先级,优先级值越低,优先级越高,就越快被调度器选中。我们可以修改进程的nice值,来改变它的优先级值。重要的进程需要让其尽快完成,也就是需要给它多分配一些CPU的执行时间。次要的先往后稍稍。
对于CPU密集型的进程,我们应该尽可能调低它的优先级,因为只要你给它,它就会尽可能的使用CPU时间,不过这个操作一般情况下内核自己会完成,它会通过算法实时去计算进程占用CPU的时间,动态调整占用CPU时间较长的进程的优先级。
内核可以强制在进程上的时间片耗尽的情况下收回CPU使用权,并将CPU交给调度器选中的下一个进程,这种方式就称为“抢占式多任务处理”。这时候前一个进程还没有运行完成就被收回了CPU使用权,但在未来的某个时间还是会被调度器选中的,所以内核会将每个进程中止时的运行环境保存下来(保存在内核所占用的内存中),这称为保护现场。当再次被调度器选中恢复运行时,会将原来保存的运行环境加载到CPU上继续执行,这称为恢复现场。
调度器选中下一个进程后,会进行底层的任务切换,也就是上下文切换。切换不应太频繁,太频繁会导致保护和恢复现场的时间过长而影响CPU的工作效率,因为这段时间CPU是没有工作的;切换也不应太慢,太慢会导致下一个进程要很久才能“上位”,比如你执行了cd命令,很久才得到响应,这显然不合理。
CPU的衡量单位是时间,内存的衡量单位是空间。CPU的百分比值等于“进程占用CPU时间/CPU总时间”。
父进程和子进程
子进程由父进程衍生。系统会为每一个进程分配一个唯一的PID,在A进程的环境下执行或者调用程序,这个程序产生的B进程就是子进程。子进程的PPID就是它父进程的PID,可以通过ps命令查看相关信息。父进程可以衍生出多个子进程,CentOS 6 上所有进程的父进程是init,CentOS 7上所有进程的父进程是systemd。Linux上创建进程的方式有三种:
- fork方式:fork是复制进程,它会复制当前进程的副本给所谓的子进程,父子进程掌握的资源是一样,包括内存中资源。但是父子进程是相互独立的, 它们是一个程序的两个实例。
- exec方式:exec是加载另一个程序来替代当前的进程,也就是在不创建新进程的情况下加载一个新程序(偷梁换柱)。创建新进程时,为了保证进程的安全,都会先fork一份当前进程,然后在此基础上调用exec来加载新程序替代该子进程。例如在bash下执行cp命令,会先fork出一个bash,然后再exec加载cp程序覆盖子bash进程变成cp进程。但要注意,fork进程时会复制所有内存页,但使用exec加载新程序时会初始化地址空间,这意味着复制的动作完全是多余的,这肯定会影响工作效率,因为地址空间的数据也是有几兆到十几兆不等的。写时复制(Copy On Write,COW)技术的出现完美解决了这个问题(COW下面会说)
- clone方式:clone的工作原理和fork相同,但是clone出来的子进程不独立与父进程,它会和父进程共享某些资源,在clone进程的时候需要指定共享哪些资源。
一般情况下,兄弟进程之间是相互独立不可见的,但是通过“管道”可以实现两者之间传递数据。
写时复制技术
在fork之后exec之前,内核会为新的子进程创建虚拟空间结构,它们复制于父进程的虚拟空间结构,但不为这些段(正文段、数据段、堆、栈)分配物理内存,它们以只读方式共享父进程的物理内存空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。直白的说,就是只fork一个空结构给子进程,实际的物理空间以只读的方式共享使用父进程的,当子进程的相应段被改变时才会复制父进程相应段的资源,用啥拿啥,不用不拿。这分为两种情况:
- fork之后没有运行exec。内核会为子进程的数据段、堆栈段分配相应的物理空间,使子进程独立于父进程。而正文段会共享父进程的物理空间(两者代码是相同的)。
- fork之后运行了exec。那就是加载了新程序替代了原本的子进程,此时正文段的代码肯定是改变了,所以也会分配单独的物理空间。
进程状态
- 新状态:就是新进程
- 就绪态:进程处于等待队列中,等待调度器的选中。
- 运行态:调度器选中它,开始运行。
- 睡眠态:处于运行台的进程因需要等待某事件(IO等待或者信号等待)的出现而无法执行,进入睡眠。睡眠态分为可中断睡眠和不可中断睡眠。可中断睡眠允许接收外界信号和内核信号来唤醒进程,我们用ps或者top命令捕捉到的大多数都是可中断睡眠;不可中断睡眠只能由内核信号唤醒,主要表现在与硬件交互的时候,比如用cat查看文件内容时,是不可以被中断的,不知道有没有人遇到cat只能看一部分文件内容。
- 终止态:进程运行完毕,返回退出状态码。
- 僵尸态:进程已经终止,但内核还没来得及删除进程列表中的项。
各状态之间的转换方式:
- 新状态 ——> 就绪态:新进程加入等待队列。
- 就绪态 ——> 运行态:处于等待队列的进程被调度器选中。
- 运行态 ——> 睡眠态:比如你yum不加-y参数安装软件时,有yes的提示,那一刻进程已经进入睡眠态,需要等到yes的信号才能进入就绪态。
- 睡眠态 ——> 就绪态
- 运行态 ——> 就绪态:内核分配的CPU时间片用完了,或者是被高优先级的进程抢占。
- 运行态 ——> 终止态
- 终止态 ——> 僵尸态
举例分析进程状态转换过程
还是以cp命令为例。在当前bash环境下执行cp命令,首先fork出一个bash子进程,然后在子bash上exec加载cp程序,cp子进程进入等待队列,由于在命令行下敲的命令,所以优先级较高,调度器很快选中它。在cp这个子进程执行过程中,父进程bash会进入睡眠状态等待被唤醒(进程等待),此刻bash无法和人类交互。当cp命令执行完毕,它将自己的退出状态码告知父进程,此次复制是成功还是失败,然后cp进程自己消逝掉,父进程bash被唤醒再次进入等待队列,并且此时bash已经获得了cp退出状态码。根据状态码这个"信号",父进程bash知道了子进程已经终止,所以通告给内核,内核收到通知后将进程列表中的cp进程项删除。至此,整个cp进程正常完成。
假如cp这个子进程复制的是一个大文件,一个cpu时间片无法完成复制,那么在一个cpu时间片消耗尽的时候它将进入等待队列。
假如cp这个子进程复制文件时,目标位置已经有了同名文件,那么默认会询问是否覆盖,发出询问时它等待yes或no的信号,所以它进入了睡眠状态(可中断睡眠),当在键盘上敲入yes或no信号给cp的时候,cp收到信号,从睡眠态转入就绪态,等待调度类选中它完成cp进程。
在cp复制时,它需要和磁盘交互,在和硬件交互的短暂过程中,cp将处于不可中断睡眠。
假如cp进程结束了,但是结束的过程出现了某种意外,使得bash父进程不知道它已经结束了(cp命令是不可能出现这种情况的),那么bash就不会通知内核回收进程列表中的cp表项,cp此时就成了僵尸进程。
进程类型和子shell
- 前台进程:一般命令(如cp命令)在执行时都会fork子进程来执行,在子进程执行过程中,父进程会进入睡眠,这类是前台进程。
- 后台进程:在命令结尾加上“&”符号,执行命令后会立即返回父进程,并返回该后台进程的jobid和pid,所以其父进程不会进入睡眠
- bash内置命令:此类命令非常特殊,父进程不会创建子进程来(bash不会创建子bash)执行这些命令,而是在当前bash中执行。但如果将内置命令放在管道后,则内置命令将和管道前面的命令同属一个进程组,这种情况会创建子进程。
两种特殊的脚本调用方式:exec和source。exec是加载程序替换当前进程,比如命令行执行cp命令,就会先fork一个一样的子bash,然后使用exec加载cp程序替代子bash,执行完毕后会发送退出状态码给父bash,也算是这个“子shell”退出了;而source不会开启子shell,它直接在当前shell中执行调用脚本且执行完成后不退出当前shell,所以脚本执行完毕后加载的环境变量会立马在当前shell中生效。
将进程放入后台执行会返回进程的jobid和pid,我们可以使用jobs命令查看后台运行的进程。如果要引用jobid,必须在前面加上%,例如“kill -9 %1”。另一种手动加入后台的方式是按下CTRL+Z键,这可以将正在运行中的进程加入到后台,但这样加入后台的进程会在后台暂停运行。
[root@template ~]# sleep 10s &
[1] 13225
[root@template ~]# jobs -l
[1]+ 13225 Running sleep 10s
# -l:显示进程的PID
# -r:显示处于running状态的jobs
# -s:显示处于stopped状态的jobs
[root@template ~]# sleep 20s
^Z[1] Done sleep 10s
[2]+ Stopped sleep 20s
[root@template ~]# jobs
[2]+ Stopped sleep 20s
从上面的例子中可以看到jobid后面有个“+”的标注,“+”表示执行中的任务,“-”表示调度器选中的下一个要执行的任务,从第三个任务开始就不再进行标注。结合任务的状态来看,状态为“running”没有“+”表示处于等待队列;“running”有“+”表示正在执行;“stopped表示处于睡眠状态”。
将job调到前台或者放入后台,可以使用fg和bg的命令,严格的说,是以运行状态放入前台和后台,即使原来任务是stopped状态的。格式为fg|bg %jobid,不指定jobid时操作的是当前带有“+”的任务。
[root@template ~]# vim 123 &
[1] 13328
[root@template ~]# jobs
[1]+ Stopped vim 123
[root@template ~]# fg %1
vim 123
~
使用disown命令可以从job table中直接移出一个job,并非是结束任务。而且移出job table后,任务将挂在init/systemd进程下,使其不依赖于终端。
disown [-ar] [-h] [%jobid ...]
# -h:指定该选项,将不从job table中移出job,而是将其设置为不接受shell发送的sighup信号。
# -a:如果没有指定jobid,表示对Job table中的所有job进行操作。
# -r:如果没有指定jobid,表示只对running状态的job进行操作
[root@template ~]# sleep 50s &
[1] 13303
[root@template ~]# jobs
[1]+ Running sleep 50s &
[root@template ~]# disown %1
[root@template ~]# jobs
[root@template ~]# ps -ef | grep [s]leep
root 13303 13185 0 19:42 pts/2 00:00:00 sleep 50s
[root@template ~]# ps -ef | grep [1]3185
root 13185 13181 0 19:02 pts/2 00:00:00 -bash
进程类型和子shell
工作中我们可能会遇到数据库备份的操作,这个备份的时间可能会很久,可是马上就要下班了,如果你关掉终端,备份就中止了。所以应该使用脱离终端的后台备份方法。注意上面的“&”虽然也是放在后台,但是是与终端相关联的,一旦退出,jobs也会消失的。想要真正与终端脱离关联,需要与nohup配合使用。另一种方法是screen,这命令我不怎么熟,这就不写了。
[root@template ~]# nohup sleep 50s &
二、信号
Linux中支持多种信号,它们都以SIG字符串开头,SIG字符串后的才是真正的信号名称,信号还有对应的数值,数值才是操作系统真正认识的信号。但由于不少信号在不同架构的计算机上数值不同(例如CTRL+Z发送的SIGSTP信号就有三种值18,20,24),所以在不确定信号数值是否唯一的时候,最好指定其字符名称。
常见信号
Signal | Value | Comment |
---|---|---|
SIGHUP | 1 | 终端退出时,此终端内的进程都将被终止 |
SIGQUIT | 3 | 从键盘发出杀死进程的信号 |
SIGKILL | 9 | 强制杀死进程 |
SIGUSR1 | 10 | 用户自定义信号1 |
SIGUSR2 | 12 | 用户自定义信号2 |
SIGCHLD | 17 | 发送该信号告知父进程自己已完成,父进程收到信号将告知内核清理进程列表。该信号可以解除僵尸进程 |
SIGCONT | 18 | 发送此信号使得stopped进程进入running,该信号主要用于jobs,例如fg和bg命令都会发送该信号 |
SIGSTOP | 19 | 进程收到信号会进入stopped的状态 |
SIGTSTP | 20 | 该信号是可被忽略的进程停止信号(CTRL+Z) |
SIGHUP
当控制终端退出时,会向该终端中的进程发送sighup信号,因此该终端上运行的shell进程、其他普通进程以及任务都会收到sighup信号而导致进程终止。多种方式可以改变因终端中断发送sighup而导致子进程也被结束的行为,这里仅介绍比较常见的三种:
- 使用nohup &的方式忽略所有SIGHUP信号
- 将待执行的命令放入子shell中的后台运行,例如“(sleep 10s &)”
- 使用disown -h设置为不接收终端发送的sighup信号。
SIGCHLD
一个编程完善的程序,在子进程终止、退出的时候,内核会发送SIGCHLD信号给其父进程,告诉父进程自己使命已完成,父进程收到信号就会对该子进程进行善后(接收子进程的退出状态、释放未关闭的资源),同时内核也会进行一些善后操作(比如清理进程表项、关闭打开的文件等)。
正常情况下,一个进程结束后都会处于一个短时间的僵尸态(发送SIGCHLD信号到父进程收到该信号并做出处理之间),只不过这个时间极短,几乎不会被ps或者top命令捕捉到。特殊情况下,子进程终止,父进程没有收到SIGCHLD信号,那此进程就会处于永久的僵尸态。僵尸不死,人类就要倒霉,僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能。虽说内核会定时清理自己下面的各种僵尸进程,但是僵尸进程有其父进程打掩护(父进程没有收到SIGCHLD信号,不知道子进程变僵尸进程),内核是见不到僵尸进程的,且直接发送信号给僵尸进程也是没有作用的,因为僵尸进程是已经终止的进程,收不到信号。那怎么处理僵尸进程呢?两种办法:
-
直接杀死僵尸进程的父进程,俗话说:子不教,父之过。。。未免残忍了点。
-
手动发送SIGCHLD的信号给僵尸进程的父进程。父进程不知道自己的孩子变僵尸,我们手动告诉它,然后它再通知内核来处理。
手动发送信号
使用kill命令手动发送信号给指定的进程
[root@template ~]# man kill
SYNOPSIS
kill [-s signal|-p] [-q sigval] [-a] [--] pid...
kill -l [signal]
# 列出所有支持的信号
[root@template ~]# kill -l
# 示例
[root@template ~]# tailf .bashrc
# 另开一个窗口
[root@template ~]# ps -ef | grep [t]ailf
root 13549 13525 0 20:51 pts/0 00:00:00 tailf .bashrc
[root@template ~]# kill -KILL 13549 或者 kill -9
参考资料
本篇博客借鉴了骏马金龙大佬的博客,自己也总结了一部分,其实说是借鉴,不如说抄袭......因为本文中一些内容我是照搬的(我会通知作者),我实在想不出更恰当的词汇。惭愧!惭愧的一笔啊!
写作不易,转载请注明出处,谢谢~~