Linux学习之进程

进程

进程process是指正在执行的程序;是程序正在运行的一个实例。它由程序指令,和从文件、其它程序中读取的数据或系统用户的输入组成。

进程状态

在进程的生命周期内,进程总会从一个状态转变到另一个状态。Linux中,一个进程有下面的可能状态:

  • Running:正在运行(它是系统中的当前进程)或准备运行(它正在等待分配CPU单元)
  • Waiting:正在等待某个事件的发生或者系统资源。另外,内核也会区分两种不同类型的等待进程:
    • 可中断等待进程:interruptible waiting processes,可以被信号中断
    • 不可中断等待进程:uninterruptible waiting processes,正在等待硬件条件,不能被任何事件/信号中断
  • Stopped:被停止de,通常是由于收到一个信号。如,正在被调试的进程
  • Zombie:已死亡,已经停止,但进程表Process Table中仍然有它的条目
    Linux学习之进程_第1张图片

分类

从不同的角度来理解,进程有不同的分类:前台/后台进程、父/子进程。

前台/后台进程

前台进程

亦交互式进程,这些进程由终端会话初始化和控制。换句话说,需要有一个连接到系统中的用户来启动这样的进程;它们不是作为系统功能/服务的一部分自动启动。

在终端terminal打开的任务就是前台进程,终端窗口关闭或网络连接失败后,进程就会中断。因为用户注销或者网络断开时,SIGHUP信号会被发送到会话所属的子进程,而此SIGHUP的默认处理方式是终止收到该信号的进程。所以若程序中没有捕捉该信号,当终端关闭后,会话所属进程就会退出。

后台进程

亦非交互式/守护进程/自动进程,这些进程没有连接到终端;不需要任何用户输入。

实现后台执行的目的,实际上是要完成如下两个目标:

  1. 使进程让出前台终端,让我们可以继续通过终端与系统进行交互。
  2. 使进程不再受终端关闭的影响,即系统在终端关闭后不再向进程发送SIGHUP信号或即使发送信号程序也不会退出。

父子进程

  • 父进程:在运行时创建其它进程的进程。
  • 子进程:在运行时由其它进程创建的进程。

在Unix/Linux系统中,大多情况下,子进程是通过父进程fork创建。系统调用一次fork,返回两个值,失败返回-1,成功时在子进程返回0,父进程返回所创建子进程的pid。

子进程创建后,子进程的结束和父进程的运行是一个异步过程,也就是说父进程没办法预测子进程什么时候结束。当一个子进程完成它的工作终止之后,其父进程需要调用wait()waitpid()去获取子进程的终止状态。

进程操作

包括创建、查找、杀死。

创建进程

Linux中创建进程有三种方式:

  1. fork()
    使用fork()函数以父进程为蓝本复制一个进程,其PID号与父进程PID号不同。Linux环境下,fork()是以写复制实现的,新的子进程的环境和父进程一样,只有内存与父进程不同,其他与父进程共享,只有在父进程或者子进程进行修改后,才重新生成一份。
  2. system()
    system()函数会调用/bin/sh –c command来执行特定的命令,并且阻塞当前进程的执行,直到command命令执行完毕。新的子进程会有新的PID。
  3. exec()
    exec()方式有若干种不同的函数,与前面两种不同,exec()方式会用新进程代替原有的进程,系统会从新的进程运行,新进程PID值与原进程PID值相同。

查找进程

Linux是一个多用户系统,不同的用户可以在系统上运行各种各样的程序,内核必须唯一标识程序运行的每个实例。

查找某个进程的进程ID:pidof systemd
查找当前shell的进程ID:echo $$
查找父进程的进程ID:echo $PPID

pidof

Process identifier of,用于查找服务进程的PID。在没有pidof命令之前,想要获知一个进程的PID号码,只得先用ps命令获得所有进程状态,再使用grep命令进行过滤查找,操作复杂且效率也很低。

pidof nginx等价于ps -ef | grep nginx

选项:
-s:仅返回一个进程号;
-c:仅显示具有相同“root”目录的进程;
-x:显示由脚本开启的进程;
-o:指定不显示的进程ID。

杀死进程

杀死进程的命令行工具有很多。

kill

传递一个PID来杀死进程

pkill

使用正则表达式作为输入

killall

默认情况下,它精确地匹配参数名,然后杀死匹配进程。killall命令将向一个或一组进程发送一个SIGTERM信号,也可以通过参数发送一个指定的信号。
基本用法

killall [-Z CONTEXT] [-u USER] [ -eIgiqrvw ] [ -SIGNAL ] NAME...
killall -l, --list
killall -V, --version

选项
-I,默认情况下,killall命令是大小写敏感的,使用-I选项将忽略大小写。
u,终止某个用户所运行的进程,可以杀死以满足某个正则表达式的一组进程,同样也可以杀死某个用户运行的所有进程;如果想杀死系统进程需要使用sudo,但是,这个选项要慎用,因为它会把该用户所有进程,包括终端进程,全部杀死,将导致该用户直接退出。所以,如果不想挨揍的话不要轻意尝试这个选项。
-o,即older,杀死运行时间超过5h的进程:killall -o 5h
-y,即younger,杀死进行时间小于4m的进程:killall -y 4m。这两个选项同样非常粗暴,也会把终端退出!
-q,表示quite,即关闭命令执行回显。默认情况下,killall 会告诉你命令执行情况,但如果不关心它的执行结果,只想静默执行,加上-q选项即可。
-i,表示interactive,交互式操作,用于不太放心杀死多个进程时,能够一一提醒匹配到的线程,然后可以自由决定该单个进程是否被杀死。
-l,查看killall所支持的所有信号。
-w,代表wait,用于确定某进程已经被杀死后才返回执行结果的情况下。实际执行时,发现执行结果会在一两秒后出现,不加-w选项,执行结果马上就显示。

实例

# 有3 个运行中进程linux1, linux2, linux3,批量杀死进程:
killall linux*
# 列出所有支持的信号,默认情况下,killall 命令将发送 SIGTERM 信号,使用 -l 选项查看 killall 所支持的所有信号:
killall -l
HUP INT QUIT ILL TRAP ABRT IOT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM
STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH IO PWR SYS
UNUSED
你可以使用 -s 选项(后面跟一个信号名)来向一个进程发送特殊信号。

概念

进程组

进程组是一个或多个进程的集合,进程组方便对多个进程的控制。它的ID由它的组长进程的进程ID决定。组长进程创建进程组,但它并不能决定进程组的存活时间,只要进程组内还有一个进程存在,进程就存在,与组长进程是否已终止无关。

作业

在终端里运行的命令都可以理解为一个作业,有的占用前台终端,有的在后台默默执行。作业的概念与进程组类似,同样由一个或多个进程组成,它分为前台作业和后台作业,一个会话会有一个前台作业和多个后台作业,与进程组不同的是,作业内的某个进程产生的子进程并不属于这个作业。

被放在后台的进程执行时间过长,而又忘记使用nohup命令,那么终端一旦断开,进程又需要被重新执行。
直接开启某个进程,又想在不中断进程的情况下让它让出前台终端。

jobs

作业的基础命令,用它可以查看正在运行的作业的信息;前面[ ]内的数字是作业ID,也是后面我们要操作作业的标识,然后是作业状态和命令。

ctrl+z

ctrl+z严格来说并是作业命令,它只是向当前进程发送一个 SIGSTOP 信号,促使进程进入暂停(stopped)状态,此状态下,进程状态会被系统保存,此进程会被放置到作业队列中去,而让出进程终端。使用它,可以暂停正在占用终端的进程而不停止它,从而让我们使用终端命令来操作此进程。

bg

即backgroud,bg %id把作业放到后台进程中执行。
结合ctrl+zbg命令,可以解决上面提出的第一个问题,不停止地将正在占用终端的进程放到后台执行。

fg

fgbg相对,使用它可以把作业放到前台来执行。

disown

disown用来将作业从作业列表中移除,即使它不属于会话,这样终端关闭后不再向此作业发送 SIGHUP 信号,以阻止终端对进程的影响。
使用disown可以解决上面提出的第二个问题,不重新执行将一个没使用nohup命令的进程不受终端关闭影响。

会话

会话,session,是一个或多个进程组的集合,它开始于用户登陆终端,结束于用户退出登陆。指用户与系统的一次对话的全程。

会话包括控制进程(与终端建立连接的领头进程),一个前台进程组和任意后台进程组。一个会话只能有一个控制终端,通常是登录到其上的终端设备或伪终端设备,产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程。

SIGHUP

特殊进程

init进程

init进程是系统中所有进程的父进程,启动Linux系统后第一个运行的程序;管理着系统上的所有其它进程。它由内核自身启动,因此理论上说它没有父进程。init进程的进程ID总是为1。它是所有孤儿进程的收养父母(它会收养所有孤儿进程)。

孤儿进程

当一个进程的父进程结束时,但是它自己还没有结束,那么这个进程将会成为孤儿进程。最后孤儿进程将会被init进程(进程号为1)的进程收养,当然在子进程结束时也会由init进程完成对它的状态收集工作,因此一般来说,孤儿进程并不会有什么危害。

例子:在main函数中,创建子进程,然后让父进程睡眠1s,让子进程先运行打印出其进程id(pid)以及父进程id(ppid);随后子进程睡眠3s(此时会调度到父进程运行直至结束),目的是让父进程先于子进程结束,让子进程有个孤儿的状态;最后子进程再打印出其进程id(pid)以及父进程id(ppid);观察两次打印其父进程id(ppid)的区别。

#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
	pid_t pid;
	pid = fork();//创建子进程
	if (pid < 0)//创建失败
	{
		perror("fork failed");
		exit(1);
	}
	if (pid = 0)//子进程
	{
		printf("child process.\n");
		// 输出进程id和父进程id
		printf("pid: \%d\t ppid:%d\n", getpid(), getppid());
		// 子进程睡眠3s,保证父进程先退出,此后子进程成为孤儿进程
		sleep(3);
		printf("now pid: %d\t ppid: %d\n", getpid(), getppid());
		printf("child process exited.\n");
	}
	else // 父进程
	{
		printf("father process.\n");
		// 为保证子进程先运行,让父进程睡眠1s
		sleep(1);
		printf("father process exited.\n");
	}
	return 0;
}

Linux学习之进程_第2张图片
从运行结果来看:当其父进程结束后,子进程成为孤儿进程,其父进程id(ppid)为1,也就是说,init进程成为该子进程的父进程。

守护进程

守护进程daemon,就是一直在后台运行的进程。一般在系统启动时启动,系统关闭时停止,没有控制终端,也不会输出。如服务器、fpm 等进程就是以守护进程的形式存在的。

创建一个守护进程的步骤:

  1. fork 子进程,退出父进程,子进程作为孤儿进程被 init 进程收养;
  2. 使用 setsid,打开新会话,进程成为会话组长,正式脱离终端控制;
  3. 设置信号处理(特别是子进程退出处理);可选项:
  4. 使用 chdir 改变进程工作目录,一般到根目录下,防止占用可卸载文件系统;
  5. 用 umask 重设文件权限掩码,不再继承父进程的文件权限设置;
  6. 关闭父进程打开的文件描述符;

僵尸进程

正常情况下,启动一个程序,开始任务,等任务结束就停止这个进程。进程停止后,该进程就会从进程表中移除。可通过System-Monitor查看当前进程。

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的某些信息如进程描述符即使执行完生命周期,仍然保存在系统(进程表)中,称之为僵死进程(Zombie Processes)。

当运行一个程序时,它会产生一个父进程以及很多子进程。 所有这些子进程都会消耗内核分配给它们的内存和 CPU 资源。这些子进程完成执行后会发送一个 Exit 信号然后死掉。这个 Exit 信号需要被父进程所读取。父进程需要随后调用 wait 命令来读取子进程的退出状态,并将子进程从进程表中移除。

若父进程正确第读取子进程的 Exit 信号,则子进程会从进程表中删掉。
但若父进程未能读取到子进程的 Exit 信号,则这个子进程虽然完成执行处于死亡的状态,但也不会从进程表中删掉。

少量僵尸进程对系统并无害。僵尸进程并不做任何事情,不会使用任何资源,也不会影响其它进程。不过由于进程表中的退出状态以及其它一些进程信息(进程描述符、进程id等)也是存储在内存中,故太多僵尸进程也会有问题。系统可用进程id有限,如果系统存在大量的僵尸进程占用进程id,就会导致因为没有可用的进程id,进而不能产生新的进程。

列出进程表中所有僵尸进程的详细内容:ps aux | grep Z

僵尸进程已死,SIGKILL命令无效。确保删除子僵尸的唯一方法就是杀掉它们的父进程:kill -s SIGCHLD pid;。将这里的pid替换成父进程的进程id,这样父进程就会删除所有已经完成并死掉的子进程。

例子:在main函数中,创建子进程,然后让父进程睡眠10s,让子进程先终止;这里子进程结束后父进程没有调用waitwaitpid函数获取其状态,用ps查看进程状态可以看出子进程为僵尸状态。

#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
	pid_t pid;
	pid = fork();//创建子进程
	if (pid < 0)//创建失败
	{
		perror("fork failed");
		exit(1);
	}
	if (pid = 0)//子进程
	{
		printf("child process exited.\n");
		// 先让子进程结束
		exit(0);
	}
	printf("father process.\n");
	//父进程睡眠10s,等待子进程先退出,又没有调用wait或waitpid获取其状态,子进程成为僵尸进程
	sleep(10);
	printf("father process exited.\n");
	return 0;
}

注:任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是Z。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不意味着子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

命令

&

符号&,将它附在命令后面可以使进程在后台执行,不会占用前台界面。实际上是在会话中开启一个后台作业。如果此时终端被关闭后,进程还是会退出。&符号只有让进程让出前台终端的功能,无法让进程不受SIGHUP信号的影响。

nohup

nohup使进程不受 SIGHUP 信号的影响。但在使用 nohup php test.php 后会发现,进程还会一直占用前台终端,但即使终端被关闭或连接断开,程序还是会执行,在当前文件夹下多个名为nohup.out文件。

nohup 的功能仅仅是让进程不受 SIGHUP 信号的影响,并不会让出前台终端,而且它还会在命令执行目录下建立nohup.out用以存储进程的输出。如果进程不需要输出,且不想让 nohup 创建文件,可以将标准输出和标准错误输出重定向。

常将nohup&搭配到一块使用,执行命令nohup command >/dev/null 2>&1 &,就可以放心的等待进程运行结果。

setsid

另一个让进程在后台执行的命令,作用是让进程打开一个新的会话并运行进程,使用方式为setsid command

终端关闭后进程退出是因为会话首进程向进程发送SIGHUP信号,setsid直接打开一个新会话来执行命令,原会话的终端状态就再也不会影响到此进程。

使用pstree来查看使用setsidnohup ... &两种命令来运行进程时的进程树状态。

nohup php test.php &
pstree -a |grep -C 6 test
  |-sshd
  |   `-sshd
  |       `-sshd
  |           `-bash
  |               `-sudo -s
  |                   `-bash
  |                       |-grep -C 6 test
  |                       |-php test.php
  |                       `-pstree -a

用ssh远程登陆的机器,所以test.php进程是挂在sshd进程下的。正常情况下,一旦 sshd 进程结束,则test.php也无法幸免。

setsid php test.php
pstree -a |grep -C 6 test
  |-{nscd}
  |-php test.php
  |-php-fpm
--
  |-sshd
  |   `-sshd

使用setsid 后,test.php进程已经与sshd进程同级,属于init进程的子进程。但setsid并没有为进程分配一个输出终端,所以进程还是会输出到当前终端上。

在终端中直接使用 setsid command 运行进程时,终端前台并不会被影响,command 会在后台默默运行。而在 shell 脚本中,运行 setsid 的进程会一直阻塞住,直到 command 进程执行结束。

因为setsid 在其是进程组长时会fork()一个进程,但它不会wait()它的子进程,而是立刻退出,所以在终端内直接使用setsid时,setsid作为进程组长不会占用终端界面。

而在 shell 脚本内,setsid不是进程组长,不会fork()子进程,而是由 bash来fork()一个子进程,而 bash 会wait()子进程,所以表现得像 setsidwait()子进程一样。

解决这个问题的两个办法:

  1. 使用&符号,使setsid强行到后台执行
  2. 使用.source命令由终端执行setsid

daemonize

GitHub,使程序作为守护进程运行,用C语言编写。

A tool to run a command as a daemon.

安装:brew install daemonize,或基于源码构建:

git clone http://github.com/bmc/daemonize.git
cd daemonize
sh configure
make
make install

安装成功,输入:daemonize,输出版本号和参数选项:

  • -e stderr:查看错误信息
  • -o stdout:查看输出结果

screen

tmux

通过 ps 命令可以查看到 command 为 ttyn 的就是它对应的进程,对应linux/dev/目录下的一个文件。

其他阅读

关于Linux进程你所需要知道的一切
Linux进程调度与后台进程
Linux守护进程的启动方法
什么是僵尸进程,如何找到并杀掉僵尸进程?

你可能感兴趣的:(Linux,linux)