父子进程、进程组、作业、会话
父进程
已创建一个或者多个进程
子进程
fork创建的。这个函数被调用一次但是返回两次,子进程返回0,父进程返回子进程id。
fork之后,操作系统会赋值一个与父进程完全相同的子进程,虽然是父子关系,但是更像是兄弟关系。这两个进程共享代码,但是数据空间是互相独立的,数据空间、指令、指针完全相同,子进程拥有当前父进程运行到的位置(PC相同)。
子进程拥有父进程的资源
进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))、环境、堆栈、内存、进程组号。
子进程独有
进程号、不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到);资源使用(resource utilizations)设定为0
进程组
多个进程的集合,其中有一个组长,PID等于进程组的PGID,只要这个组里面有一个进程存在那么这个组就存在,与组长进程是否终止无关
作业
shell分前后台来控制的不是进程而是作业或者进程组。
一个前台作业由多个进程组成,一个后台也由多个进程组成。shell可以运行一个前台作业和任意多个后台作业。这就是作业控制。
为什么只能运行一个前台作业?
前台作业是指当前终端窗口中运行的作业,而一个后台作业则是在后台运行的作业,不会占用当前终端窗口。
一个终端窗口只能有一个前台作业,是因为终端窗口只能和一个进程交互,用户在终端窗口输入命令,终端窗口会将命令传递给前台作业,如果有多个作业同时处于前台,那么输入的命令将被发送给哪个作业时不确定的,这会导致混乱和错误。
作业与进程组的区别
如果作业中有某个进程创建了子进程,则这个子进程是不属于该作业的,以但作业运行结束,shell就把自己提到前台(子进程还在,但是子进程不属于作业)。如果原来的前台进程还存在(这个子进程还没有终止),他将自动变为后台进程组。
当我们在前台新起了一个作业,shell就被提到了后台,因此shell就没有办法再继续接受我们的指令并且解析运行了。 但是如果前台进程退出了,shell就会有被提到前台来,就可以继续接受我们的命令并且解析运行。
会话
是一个躲着多个进程组的集合。一个会话有一个控制终端,在xshell或者winscp中打开一个窗口就是新建一个会话。
孤儿进程
如果父进程退出,子进程还没有退出,那些子进程将成为孤儿进程。孤儿进程被1号进程init进程收养。那么子进程的父进程将变成init进程。由init进程对他们完成状态收集工作。
僵尸进程
在操作系统中,当一个进程终止时,它的状态信息仍然被保留在系统中,直到其父进程调用wait或waitpid等系统调用来获取其终止状态信息。如果父进程没有及时调用这些系统调用来获取终止状态信息,那么这个已经终止的子进程就会成为一个僵尸进程(Zombie Process)。
僵尸进程占用了系统资源,因为它们仍然占用了进程ID、进程表项和一些其他资源,而这些资源可能需要被其他进程使用。因此,需要及时清除僵尸进程,以释放这些资源。
Linux中,使用ps aux查看信息,发现僵尸进程状态为“Z”
当一个进程终止时,如果它的子进程仍然处于僵尸状态,那么这些子进程的父进程ID将被重置为1,也就是init进程。此时,init进程会调用wait或waitpid等系统调用来获取这些子进程的终止状态信息,并清除它们的僵尸状态,从而释放系统资源。
需要注意的是,当父进程终止时,操作系统会将其所有未处理的信号发送给其子进程。如果子进程没有捕获这些信号,那么它们将被终止,成为僵尸进程。因此,在编写程序时,需要及时处理信号,以避免出现僵尸进程。
避免僵尸进程
- 通过signal(SIGCHID,SIG_IGN)通知内核对子进程结束不关心,由内核回收。如果想让父进程挂起,可以在父进程中加入条语句signal(SIGCHLD,SIG_IGN),表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。(可以让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用资源)
- 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
- 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
- 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
- 使用双向管道:父进程在fork子进程之前创建一个管道,并在子进程中关闭读端口,父进程关闭写端口,当子进程终止时,会向管道写入一个消息,父进程在读取到消息后,使用wait或waitpid等系统调用获取子进程的状态信息,从而避免出现僵尸进程。
进程终止
进程终止的几种方式
- main函数自然返回
- exit,属于C函数库
- _exit,属于系统调用
- 调用abort函数,异常程序终止,同时发送SIGABRT信号给调用进程
- 接收能导致进程终止的信号 ctrl+c,SIGINT
exit与_exit
图片来源阿秀的学习笔记
如何让进程后台运行
- 命令后面加上&,这样是将命令放入到一个作业队列中
- ctrl+z挂起进程,使用jobs查看序号,再使用bg%序号后台运行进程
- nohup+&,将标准输出和标准错误缺省会被重定向到nohup文件中,忽略所有SIGHUP挂断信号
- 运行指令前面加上 setsid,使其父进程编程init进程,不受HUP信号影响
- 将命令+&放在()括号中,也可以是进程不受HUP信号影响
守护进程
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性的执行某种任务。Linux大多数服务器就是用守护进程的方式实现的,比如WEB服务器。
创建守护进程
- 让程序在后台执行:调用fork()产生一个子进程,然后使父进程退出
- 调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程进程下来的,守护进程要摆脱他们,不受他们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功之后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
- 禁止进程重新打开控制终端。这时进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况,可以通过使进程不再是会话组长来实现。再一次通过fork创建新的子进程,使调用fork的进程退出。
- 关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符,如果不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误,首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
- 将当前目录更改为根目录
- 子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
- 处理SIGCHLD信号,对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程,从而占用系统资源。如果父进程等待子进程结束,将增加父进程负担。所以将SIGCHLD信号的操作设置为SIG_IGN,子进程结束的时候就不会产生僵尸进程。