[Linux Shell学习系列十三]捕获-2.进程
D25
1. 什么是进程
进程是运行在Linux中的程序的一个实例。
每当在Linux中执行一个命令,它都会创建或启动一个新的进程。
操作系统通过被称为PID或进程ID的数字编码来追踪进程。系统中的每一个进程都有一个唯一的PID。
#执行一个sleep命令,启动了一个进程,得到一个PID $ sleep 10 & [1] 22824 #查找进程名为sleep的进程 #可以看到sleep的进程在运行,其PID与执行时得到的相同 $ ps -ef | grep sleep ntrade 22824 22760 0 10:46 pts/0 00:00:00 sleep 10 #并行地从3个终端窗口运行上述sleep命令 #可以看到sleep的每一个实例都创建了一个单独的进程 #并且各自的父进程(PPID)不同 $ ps -ef|grep sleep user1 22862 22782 0 10:48 pts/1 00:00:00 sleep 10 user1 22863 22760 0 10:48 pts/0 00:00:00 sleep 10 user1 22864 22801 0 10:48 pts/2 00:00:00 sleep 10
系统中每一个用户进程都有一个父进程。
#使用ps -f列出进程的PID和父进程的PPID $ ps -f UID PID PPID C STIME TTY TIME CMD user1 22760 22759 0 10:45 pts/0 00:00:00 -bash user1 22868 22760 0 10:53 pts/0 00:00:00 ps -f
在Shell命令行提示符下运行的命令都把当前Shell的进程作为父进程。例如,你在Shell命令行提示符下输入ls命令,Shell将执行ls命令,此时Linux内核会复制Shell的内存页,然后执行ls命令。
在Unix中,每个进程是使用fork和exec方法创建的,会导致系统资源的损耗。
在Linux中,fork方法是使用写时拷贝内存页实现的,所以导致的仅是时间和复制父进程的内存页表所需的内存的损失,并且会为子进程创建一个唯一的任务结构。写时拷贝模式在创建新进程时避免了创建不必要的结构拷贝。
注:写时拷贝,转自:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝将是无用功。
Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个地址空间,而是让父进程和子进程共享一个拷贝。只有在需要写入的时候,数据才会复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间的页的拷贝被推迟到实际发生写入的时候。
在页根本不会被写入的情况下,举例来说,fork()之后立即调用exec(),它们就无需复制了,fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本不会使用的数据(地址空间常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。
2. 前台进程和后台进程
当启动一个进程时(运行一个命令),可以如下两种方式运行该进程:
1)前台进程:默认情况下运行在前台,从键盘获取输入并发送输出到屏幕;当一个程序运行在前台时,我们不能在同一命令行提示符下运行任何其他命令(其他任何其他进程),因为在程序结束它的进程之前命令行提示符不可用。
2)后台进程:启动后台进程最简单的方法是添加一个控制操作符&到命令的结尾。
#启动一个后台作业,会打印:作业编号([1])和进程号PID(3143) $ sleep 2 & [1] 3143 #作业完成时,会发送作业完成信息到终端程序 [1]+ Done sleep 2
将进程放到后台运行时,可以继续运行其他命令,而不需要等待此进程运行完成再运行其他命令。
D26
3. 进程的状态
每个Linux进程都有它的生命周期,如:创建、执行、结束和清除。
每个进程也都有各自的状态,显示进程中当前正在发生什么。进程的状态有如下几种:
D(不可中断休眠状态)——进程正在休眠且不能恢复,直到一个事件发生为止;
R(运行状态)——进程正在运行;
S(休眠状态)——进程没有在运行,而在等待一个事件或是信号;
T(停止状态)——进程被信号停止,如信号SIGINT或SIGSTOP;
Z(僵死状态)——标记为<defunct>的进程是僵死的进程,之所以残留是因为其父进程没有适当的销毁他们;如果父进程退出,这些进程将被init进程销毁。
4. 怎样查看进程
使用ps命令、pstree命令和pgrep命令查看当前进程的信息。
1)使用ps命令,可以查看当前的进程。
默认情况下,ps命令只会输出当前用户并且是当前终端(如当前Shell)下调用的进程的信息,其输出将类似如下所示:
$ ps PID TTY TIME CMD 3112 pts/0 00:00:00 bash 3159 pts/0 00:00:00 ps
默认情况下,ps会显示:
进程ID(PID);
进程关联的终端(TTY);
格式为“[dd-]hh:mm:ss”的进程累积CPU时间(TIME);
可执行文件的名称(CMD)。
并且输出内容默认是不排序的。
#使用标准语法显示系统中的每个进程 $ ps -ef UID PID PPID C STIME TTY TIME CMD #使用BSD语法显示系统中的每个进程 $ ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND #也可以使用PS_FORMAT环境变量重写默认的输出格式 #使用grep查看单个进程的信息 $ ps aux | grep sleep ntrade 3220 0.0 0.0 4364 356 pts/1 S 10:27 0:00 sleep 20
2)pstree命令以树形结构的形式显示系统中所有当前运行的进程的信息。
此树形结构以指定的PID为根,若没有指定PID,则以init进程为根。
如果pstree命令指定的参数是用户名,就会显示以此用户的进程为根的所有进程树的信息。
3)pgrep命令可以基于名称或其他属性查找进程。
pgrep命令会检查当前运行的进程,并列出与选择标准相匹配的进程的ID。
#列出user1用户运行的进程的PID $ pgrep -u user1 3111 3112 3197 3198 3227 #列出user1用户的sleep进程的PID $ sleep 20 & [4] 3230 $ pgrep -u ntrade sleep 3230
5. 向进程发送信号
使用键盘或pkill命令、kill命令和killall命令向进程发送各种信号。
1)在Bash下,使用键盘发送信号。
| 组合键 | 含义 |
| Ctrl+C | 中断信号,发送SIGINT信号到运行在前台的进程 |
| Ctrl+Y |
延时挂起信号,使运行的进程在尝试从终端读取输入时停止。 控制权返回给Shell,使用户可以将进程放在前台后后台,或杀掉该进程 |
| Ctrl+Z | 挂起信号,发送SIGSTOP信号到运行的进程,由此将其停止,并将控制权返回给Shell |
2)大多数主流的Shell包括Bash,都有内置的kill命令。
Linux系统中,也有kill命令,即/bin/kill。如果使用/bin/kill,系统可能会激活一些额外的选项,如杀掉不是你自己的进程,或指定进程名为参数,类似于pgrep和pkill命令。
不过两种kill命令默认都是发送SIGTERM命令。
但该准备杀掉一个进程或一连串的进程时,常识:从常识发送最安全的信号开始,即SIGTERM信号。
以这种方式,关掉正常停止运行的程序,当进程收到SIGTERM信号时,有机会按照已经设计好的流程执行,如清理和关闭打开的文件。
如果发送SIGKILL信号到进程,它将消除进程先清理而后关闭的机会,可能导致错误结果。但如果有序的终结不管用,发送SIGINT或SIGKILL信号可能是唯一的方法了。如一个前台进程用Ctrl+C杀不掉,最好就使用kill -9 PID了。
特别有用的信号包括:
| 信号名称 | 信号值 |
| SIGHUP | 1 |
| SIGINT | 2 |
| SIGKILL | 9 |
| SIGCONT | 18 |
| SIGSTOP | 19 |
a. 在Bash中,信号名或信号值都可作为kill命令的选项,而作业号或进程号作为kill命令的参数。
使用进程号:
$ sleep 20 & [5] 3236 #发送SIGKILL信号到该进程 $ kill -9 3236 #也可以是 $ kill -KILL 3236 #或 $ kill -SIGKILL 3236
#停止
$ [5]+ Killed sleep 20
使用作业号:
$ sleep 20 & [1] 3239 #列出当前Shell所有作业的信息 $ jobs -l [1]+ 3239 Running sleep 20 & #终结作业1 $ kill %1
#结束 $ [1]+ Terminated sleep 20
b. killall命令会发送信号到任何指定命令的所有进程。
所以,当一个进程启动了多个实例时,使用killall命令来杀掉这些进程会更方便。
注意:一些商用的Unix系统中killall命令工作方式可能不同,需要先测试。
如果没有指定信号名,killall命令会默认发送SIGTERM信号。
#杀掉所有firefox进程 $ killall firefox
c. 使用pkill命令,可以通过指定进程名、用户名、组名、终端、UID、EUID和GID等属性来杀掉相应的进程。
pkill命令默认也是发送SIGTERM信号。
#杀掉所有用户的firefox进程 $ pkill firefox #强制杀掉用户user1的firefox进程(发送KILL信号) $ pkill -KILL -u user1 firefox #让sshd守护进程重新加载其配置文件 $ pkill -HUP sshd
6. 关于子Shell
子Shell是由Shell或Shell脚本运行的子进程。当你在Shell命令行提示符下,运行一个Shell脚本时,会创建一个叫做子Shell的新进程,你的脚本将会使用这个子Shell来运行。
子Shell是命令处理程序(提供给你命令行提示符的Shell或是一个xterm窗口)的一个单独实例。就像你的命令在命令行提示符下被解释,类似的,脚本批处理一连串命令。
每个运行的Shell脚本都是父Shell的子进程。
Shell脚本可以自己启动子进程,这些子Shell让脚本可以做并行处理,实际上是同时执行多个子任务。
$ cat subshell_test.sh #!/bin/bash #202006 ( #内部圆括号,即是一个subshell while [ 1 ] #无限循环 do echo "`date` SubShell running..." done ) #脚本会一直运行,不会退出 exit $? #在一个终端执行脚本 $ ./subshell_test.sh #在另外一个终端查看 #其中PID为3284的进程是subshell_test.sh自身; #PID是3285的进程就是其子Shell $ ps -ef|grep subshell_test.sh ntrade 3284 3198 0 12:07 pts/1 00:00:00 /bin/bash ./subshell_test.sh ntrade 3285 3284 7 12:07 pts/1 00:00:03 /bin/bash ./subshell_test.sh
通常,脚本中的外部命令会分支出一个子进程,而Bash的内部命令不会。由此,Bash的内部命令比外部命令执行地更快,并使用更少的系统资源。
内嵌在圆括号内的命令列表被作为一个子Shell运行,语法:
(command1; command2; ...)
子Shell中的变量在其代码块之外是不可见的,不能被传到启动这个子Shell的Shell(父进程)。实际上,这些变量是子Shell的本地变量。
$ cat subshell_var.sh #!/bin/bash #202006 echo "We are outside the subshell." echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL."
#BASH_SUBSHELL是Bash的内部变量,指示子Shell的嵌套深度
echo; echo outer_variable=Outer global_variable= #先定义一个变量,看能否存储子Shell设置的值 ( echo "We are inside the subshell." echo "Subshell level INSIDE subshell = $BASH_SUBSHELL." inner_variable=Inner echo "From inside subshell, inner_variable = $inner_variable." echo "From inside subshell, outer_variable = $outer_variable." global_variable=$inner_variable echo "From inside subshell, global_variable = $global_variable." export global_variable ) echo; echo echo "We are outside the subshell." echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL." echo if [ -z "$inner_variable" ] #inner_variable的值的长度为0 then echo "From outside subshell, inner_variable is undefined." else echo "From outside subshell, inner_variable is defined." fi echo "From outside subshell, global_variable = $global_variable" echo
执行结果如下:
$ ./subshell_var.sh We are outside the subshell. Subshell level OUTSIDE subshell = 0. We are inside the subshell. Subshell level INSIDE subshell = 1. From inside subshell, inner_variable = Inner. From inside subshell, outer_variable = Outer. From inside subshell, global_variable = Inner. #在子Shell中被赋值 We are outside the subshell. Subshell level OUTSIDE subshell = 0. From outside subshell, inner_variable is undefined. #子Shell的本地变量,在主代码块中未定义 From outside subshell, global_variable = #在子Shell中赋值没有传递到主代码块,即使是全局变量
利用子Shell的特性,可以使用子Shell为一些命令集合设置一个专用环境,如:
COMMAND1 COMMAND2 ( IFS=: PATH=/bin unset TERMINFO set -C shift 5 COMMAND4 COMMAND5 exit 3 #只是退出这个子Shell ) COMMAND6
上面提到了内部变量BASH_SUBSHELL,还有一个SHLVL,示例如下:
$ cat valueof_bash_subshell.sh #!/bin/bash #202006 echo "\$BASH_SUBSHELL outside subshell = $BASH_SUBSHELL" ( echo "\$BASH_SUBSHELL inside subshell = $BASH_SUBSHELL" ) ( ( echo "\$BASH_SUBSHELL inside nested subshell = $BASH_SUBSHELL" ) ) echo #SHLVL是Bash的内部变量,指示Bash的嵌套深度。 #在命令行下,SHLVL为1 #在脚本中,SHLVL的值增加为2 echo "\$SHLVL outsid subbash = $SHLVL" ( echo "\$SHLVL outsid subbash = $SHLVL" ) #执行 $ ./valueof_bash_subshell.sh $BASH_SUBSHELL outside subshell = 0 $BASH_SUBSHELL inside subshell = 1 $BASH_SUBSHELL inside nested subshell = 2 $SHLVL outsid subbash = 2 $SHLVL outsid subbash = 2
本节结束

浙公网安备 33010602011771号