最近在写一个调用外部脚本的组件,其中涉及了进程相关及Java调用外部程序的一些知识点,整理下发出来。
第一篇先说进程相关,顺道复习下操作系统,以下全部以Linux环境为背景,进行介绍。
本文主要核心:
1、进程的类型&结构&状态。
2、fork、exit、SIGCHLD、wait
3、产生孤儿进程&僵尸进程的原因。
4、核心问题
概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。-- --百度百科
一、进程的类型&结构&状态
进程的分类:
按照作业类型分类
守护进程:
执行特定功能或者执行系统相关任务的后台进程。守护进程只是一个特殊的进程(注意不是内核的组成部分,只是某些进程恰好在系统启动时启动,直到系统关闭时才停止运行,而某些守护进程只是在需要时才会启动,比如Apache服务等,可以在需要的时候才启动该服务)
批处理进程:
是一个进程序列。该进程负责按照顺序启动其它进程。
交互进程:
是由shell启动的进程,它既可以在前台运行,也可以在后台运行。交互进程在执行过程中,要求与用户进行交互操作。
按照进程状态分类
守护进程:
守护进程没有控制终端,所有守护进程都可以超级用户(用户ID为0)的优先权运行。
孤儿进程:
一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
僵尸进程:
一个子进程结束,但是没有完全释放内存(在内核中的 task_struct没有释放),该进程就成为僵尸进程。
其中僵尸进程如果过多会导致严重的资源浪费,但孤儿进程不会。因为当init进程领养孤儿进程后,会负责孤儿进程的管理及释放。
进程构成:
1、PCB(进程控制段)
2、数据段
3、正文段
PCB:
PCB通常分为两块,一块常驻内存通常包括进程状态、优先数、过程特征、数据段始址、等待原因和队列指针,这部分主要是处理器进行调用的必须信息。另一部分就是非必须信息,当进程不占有处理器时,系统不会对这部分信息进行查看,通常存放在磁盘的对换区。
数据端:
这部分通常分为三块,用户栈区(供用户程序使用的信息区);用户数据区(包括用户工作数据和非可重入的程序段);系统数据区(包括系统变量和对换信息)
正文段:
存放进程的主要数据信息,并且正文段属于可重入程序,能够被其他进程所共享,Linux中存在一种叫做正文表的结构,用来记录每个正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。
进程状态:
D:不可中断的sleep
R:运行状态
S:可中断睡眠
T:暂停
W:分页状态
X:死亡状态
Z:僵尸进程
<:高优先级别
N:低优先级别
L:页锁定
s:Session Leader
l:多线程
+:前台进程
二、fork、exit、SIGCHLD、wait
fork:
它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。
fork函数将运行着的程序分成2个近似一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。新创建的子进程几乎但是不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本、数据、bss段、堆、用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。最大不同恐怕就是
这是两个进程,拥有不同的PID。
与其相似的命令有:vfork、clone,与此对应的三个系统调用有:sys_fork、sys_vfork、sys_clone,这三个都依赖于do_fork来实现,只是传递的参数不同而已。而do_fork依赖于copy_process实现。具体的过程可以查阅相关资料进行了解,在此不进行详细叙述,仅描述一个大体的过程。
exit:
fork类函数创建新的进程,而exit则负责进程的退出。说一些函数调用层次:系统调用exit()在内核中是用sys_exit()来实现的;sys_exit()调用do_exit(),在对于子进程调用对应的exit之后,操作系统会传递一个SIGCHLD信号。
类似的函数有_exit(),_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构。两者最大区别为:exit()函数在终止当前进程之前要检查该进程打开过那些文件,把文件缓冲区中的内容写回文件。(在Linux的标准函数库中,有一种被称为 "缓冲I/O" 的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区.每次读文件时,会联系读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区当中读取; 同样的,每次写文件的时候,也仅仅是写入内存的缓冲区,等满足了一定的条件时,再将缓冲区中的内容一次性写入文件.这种技术大大增加了文件读写的速度,但是也给编程带来了一点麻烦. 比如有些数据你认为已经被写入到文件中,实际上因为没有满足特定的条件,他们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失. 因此,为了数据的完整性,请使用exit()函数.)
SIGCHLD:
在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。具体参数:
1,子进程已终止 CLD_EXITED
2,子进程异常终止(无core) CLD_KILLED
3,子进程异常终止(有core) CLD_DUMPED
4,被跟踪子进程以陷入 CLD_TRAPPED
5,子进程已停止 CLD_STOPED
5,停止的子进程已经继续 CLD_CONTINUED
wait:
pid_t wait(int *status)
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉。好像status几乎是被忽略的。
三、产生孤儿进程&僵尸进程的原因
孤儿进程形成的原因:
父进程先于子进程结束,那么子进程就没有了父进程,时候系统释放了父进程的所有资源,子进程就会成为init进程的子进程。孤儿进程没有任何危害,父进程的退出也会通过信号的方式通知子进程。
僵尸进程形成原因:
僵尸进程是由于子进程先于父进程退出,子进程的资源已经释放,但子进程在系统的进程管理树种占用一个节点;系统保留此节点的意义在于,让父进程处理子进程的退出;子进程退出时发送信号给父进程,便于父进程处理子进程完成的数据,做到多任务协调工作。但如果父进程未调用wait / waitpid,对终止的子进程进行善后处理,这部分资源就得不到对应释放了。
处理方式:
1、在fork子进程时一定要记住wait它们
2、在子进程退出时,操作系统肯定会给父进程一个SIGCHLD信号,我们可以专门为SIGCHLD创建一个处理函数专门用于调用wait或waitpid
说到这里好像后面的模块应该就都懂了。
四、两个核心问题:
1、如何避免僵尸进程
在子进程退出时,操作系统肯定会给父进程一个SIGCHLD信号,我们可以专门为SIGCHLD创建一个处理函数专门用于调用wait或waitpid。
2、如何避免孤儿进程
1、在父进程被kill时,先杀死子进程
2、强势点,直接全部杀死。(ps:
#!/bin/sh
# kill process and child process
ps --ppid $1| awk '{if($1~/[0-9]+/) print $1}'| xargs kill -9
kill -9 $1
)
3、当杀死父进程时,仍有子进程还在执行,那么将变为孤儿进程,由守护进程负责管理及终止。




