1. 代码模拟实现僵尸进程, 孤儿进程的场景.
1.1 创建进程
在Linux中主要提供了fork、vfork、clone三个进程创建方法。
在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。
通过系统调用创建进程——fork
子进程和父进程谁先执行决定于谁先从就绪状态抢占到了CPU,并没有确定的先后顺序。
fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。
注意:
- 调用fork后,父子进程交替运行
- 子进程死亡,成为一种僵尸状态,僵尸是有害的。
- 父进程死亡,子进程存在,就是孤儿进程。
- 孤儿进程被1号进程领养,1号进程也叫孤儿院。
在fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。例:
运行结果如下:
fork与vfork的区别:
- fork的父子进程是交替运行的,vfork的父进程等待子进程结束才执行
- fork写时拷贝,vfork写时不拷贝
- vfork比写时拷贝的fork性能还高
- vfork必须要使用exit或exec系列
- vfork在任何一个系统上的实现都是有问题的,故而不要使用
系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid, Vfork和fork则是在父进程中返回子进程的进程号,在子进程中返回0。
小结
fork
1)调用方法
#include “sys/types.h”
#include “unistd.h”
pid_t fork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1
2) fork函数调用的用途
一个进程希望复制自身,从而父子进程能同时执行不同段的代码。
vfork
1) 调用方法
与fork函数完全相同
#include “sys/types.h”
#include “unistd.h”
pid_t vfork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1
2)vfork函数调用的用途
用vfork创建的进程主要目的是用exec函数执行另外的程序。
clone
1)调用方法
#include “sched.h”
int clone(int (fn)(void ), void *child_stack, int flags, void *arg);
正确返回:返回所创建进程的PID,函数中的flags标志用于设置创建子进程时的相关选项,具体含义参看P25
错误返回:-1
2)clone()函数调用的用途
用于有选择地设置父子进程之间需共享的资源
fork,vfork,clone的区别
1)fork出来的子进程是父进程的一个拷贝,即,子进程从父进程得到了数据段和堆栈段的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存的方式访问;而vfork则是子进程与父进程共享内存空间, 子进程对虚拟地址空间任何数据的修改同样为父进程所见;clone则由用户通过参clone_flags 的设置来决定哪些资源共享,哪些资源拷贝。
2)fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。
1.2 僵尸进程
1.2.1 概念
(1)僵尸进程是一个比较特殊的状态,当进程退出父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵尸进程。即当子进程退出,父进程没有读取到子进程退出的退出码时就会产生僵尸进程。僵尸进程会在以终止状态保持在进程表中,并且会一直等待父进程读取退出码状态。
(2)一个进程在其父进程没有调用wait()或者waitpid()的情况下退出,这个子进程就是僵尸进程。如果其父进程还存在而一直不调用wait(),则僵尸进程无法回收,等到其父进程退出后该进程将被init收回。
1.2.2 模拟实现僵尸进程
运行结果如下:
1.2.3 僵尸进程的危害
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题。如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程
1.2.4 如何避免僵尸进程的危害
1) 父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
执行wait()或waitpid()系统调用,则子进程在终止后会立即把它在进程表中的数据返回给父进程,此时系统会立即删除该进入点。在这种情形下就不会产生defunct进程。
2) 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler。在子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
3) 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号
4)fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
1.3 孤儿进程
1.3.1 概念
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将会成为孤儿进程。孤儿进程将会被1号进程init进程收养,并由init进程对它们完成状态收集工作。
1.3.2 模拟实现孤儿进程
运行结果如下:
2. 使用setenv, export等环境变量相关的函数和命令.
Linux中两个重要的环境变量:
1)PATH:可执行程序的查找路径, echo $PATH来查看。
设置方法:
export PATH = PATH:xxx 但是登出后即失效。
setenv PATH = PATH:xxx 该子shell执行后即失效。
将export PATH = PATH:xxx 写在~/bashrc, ~/.bash_profile, /etc/profile等皮配置文件中,然后执行source shell。
2)LD_LIBRARY_PATH:动态库的查找路径,设置方式与PATH类似。
setenv函数:改变或增加环境变量,相关的函数getenv, putenv, unsetenv。通过此函数不能增加或修改shell进程的环境变量或者说通过setenv函数设置的环境变量只在本进程而且是本次执行中有效。一般写在执行的shell文件中。
export命令:export [-n]变量名 = 变量值;当子进程被触发时,export命令使得父进程的变量可以通过复制传递给子进程。执行一个脚本时会先开启一个子shell的环境,将父shell中的环境变量和使用export的变量,复制给子shell。一个shell中的系统环境变量只对该shell或它的子shell有效,子shell执行结束后,变量消失并不能返回到父shell。所以子shell中的变量变化不会反映到父shell。不用export的命令只对该shell有效,对子shell也没效果。 -n可以取消变量输出。
source执行脚本和直接执行脚本的区别:source执行脚本在当前环境中,直接执行脚本在子shell环境中。
set命令:bash内建的命令,显示当前shell中的变量。unset命令来清除某个变量。
env命令:显示当前shell中的环境变量。
关于环境变量命令介绍
1.echo 显示某个环境变量值 echo $PATH
2.export 设置一个新的环境变量 export HELLO=”hello” (可以无引号)
3.env 显示所有环境变量
4.set 显示本地定义的shell变量
5.unset 清除环境变量 unset HELLO
6.readonly 设置只读环境变量 readonly HELLO
常见的环境变量:
PATH:决定了shell将到哪些目录中寻找命令或程序
HOME:当前用户主目录
MAIL:是指当前用户的邮件存放目录。
SHELL:是指当前用户用的是哪种Shell。
HISTSIZE:是指保存历史命令记录的条数
LOGNAME:是指当前用户的登录名。
HOSTNAME:是指主机的名称,许多应用程序如果要用到主机名的话,通常是从这个环境变量中来取得的。
LANG/LANGUGE:是和语言相关的环境变量,使用多种语言的用户可以修改此环境变量。
PS1:是基本提示符,对于root用户是#,对于普通用户是$。
PS2:是附属提示符,默认是“>”。可以通过修改此环境变量来修改当前的命令符,比如下列命令会将提示符修改成字符串“Hello,My NewPrompt :) ”。
# PS1=” Hello,My NewPrompt :) “
使用修改.bashrc文件进行环境变量的编辑,只对当前用户有用。
使用修改 /etc/profile 文件进行环境变量的编辑,是对所有用户有用。