进程的状态:
1. Linux进程状态:R run 运行状态 0 (TASK_RUNNING),可执行状态&运行状态(在run_queue队列里的状态)
2. Linux进程状态:S 浅度睡眠 1 系统默认sleep (TASK_INTERRUPTIBLE),可中断的睡眠状态, 可处理signal
3. Linux进程状态:D 深度睡眠 2 硬件交互I/O (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态, 可处理signal, 有延迟
4. Linux进程状态:T stop 状态 4 (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态, 不可处理signal, 因为根本没有时间片运行代码
5. Linux进程状态:Z 僵死状态 16(TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。不可被kill, 即不响应任务信号, 无法用SIGKILL杀死
6 . linux进程状态 : X 死亡状态: 32
进程的创建:
fork()
一个函数,两个返回值 父进程返回子进程pid ,子进程返回 0 ;错误返回 -1;
子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程拥有的拷贝。父、子进程并共享这些存储部分。如果正文段是只读的,则父、子进程共享正文段。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在fork之后经常跟随着exec。作为替代,使用了写时复制(copy-on-write,cow)的技术。这些区域由父、子进程共享,而且内核将他们的存取许可权改变位只读的。如果有进程试图修改这些区域,则内核抛异常,典型的是虚存系统中的“页”,做一个拷贝。
根据copy-on-write的思想,在子进程中,改变父进程的数据时,会先 复制父进程的数据修然后再改,从而达到子进程对数据的修改不影响父进程。但是我们发现,复制的前后,其值的地址都是一样的。为什么呢?子进程拷贝的时候也拷贝了父进程的虚拟内存”页”,这样他们的虚拟地址都一样,但是对应不同的物理内存空间。
二、copy-on-write工作原理
假设进程A创建子进程B,之后进程A和进程B共享A的地址空间,同时该地址空间中的页面全部被标识为写保护。此时B若写address的页面,由于写保护的原因会引起写异常,在异常处理中,内核将address所在的那个写保护页面复制为新的页面,让B的address页表项指向该新的页面,新页面可写。而A的address页表项依然指向那个写保护的页面。然后当B在访问address时就会直接访问新的页面了,不会在访问到哪个写保护的页面。当A试图写address所在的页面时,由于写保护的原因此时也会引起异常,在异常处理中,内核如果发现该页面只有一个拥有进程,此种情况下也就是A,则直接对该页面取消写保护,此后当A再访问address时不会在有写保护错误了。如果此时A又创建子进程C,则该address所在的页面又被设置为写保护,拥有进程A和C,同时其他页面例如PAGEX依然维持写保护,只是拥有进程A、B和C。如果此时A访问PAGEX,则异常处理会创建一个新页面并将PAGEX中的内容复制到该页面,同时A相应 的pte指向该新页面。如果此时C也访问PAGEX,也会复制新页面并且让C对应的pte指向新页面。如果B再访问PAGEX,则由于此时PAGEX只有一个拥有进程B,故不再复制新页面,而是直接取消该页面的写保护,由于B的pte本来就是直接指向该页面,所以无需要在做其它工作。
fork的两种用法:
1、使父子进程执行不同的代码段;父进程等待客户端请求,子进程来处理请求;
2、一个进程要执行不同的程序;调用exec;
vfork()
1、保证子进程先运行,
2、当子进程调用exit()或exec()后,父进程往下执行。
3、子进程享有父进程的地址空间,意味着子进程修改变量会影响到父进程;
引入vfork的目的:往往子进程都会被exec掉,那么干脆暂时不给他拷贝数据啥的了(写时拷贝虽然代价下但是也有代价啊),直接让他暂时用父进程的地址空间;父进程在外面等一会儿,等到子进程运行结束。
子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子“霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。
进程的等待:
wait
1.如果其所有子进程都还在运行,则阻塞
2.如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3.如果它没有任何子进程,则立即出错返回
wait的作用
1、回收子进程,避免子进程变成僵尸进程;
2、保证父子进程的退出同步过程
3、回收子进程的退出状态
waiptpid提供了wait没有提供的三个功能:
1. waitpid可等待一个特定的进程
2. waitpid提供了一个wait的非阻塞版本
3. waitpid支持作业控制
waitpid
等待函数1:
pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数status : 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL, 4字节,按字节安排,最低8位:退出信号(系统发出的信号);次低8位:退出码;
如果进程由于接收到SIGCHLD而调用wait,则可期望wait会立即返回。但如果在任 意时刻调用wait,则进程可能阻塞。
在一个子进程 终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻 塞。
如果status不是一个空指针,则终止进程的终止状态就存放在它所指的单元内。如果不 关心终止状态,则可将该参数设为空指针(waitpid同样适用)。
等待函数2:
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
1. 当正常返回的时候waitpid返回收集到的⼦子进程的进程ID;
2. 如果设置了选项WNOHANG,⽽而调⽤用中waitpid发现没有已退出的子进程可收集,
则返回0;
3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
4. 当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就 会出错返回,这时errno被设置为ECHILD.
参数:
1. pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
Pid==0等待其组ID等于调⽤用进程组ID的任一个子进程。
Pid<-1等待其组ID等于pid绝对值的任一子进程。
2. status:
WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。(查看进程是 否是正常退出)
WEXITSTATUS(status) : 若WIFEXITED⾮非零,提取⼦子进程退出码。(查看进程 的退出码)
3. options:
WNOHANG :若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
0:阻塞方式等待
阻塞会一种卡在那里等,非阻塞发现没有子进程退出,就返回;若正常结束,则返回该子进程的ID。
进程的替换:
不带字母p (表示示path)的exec函数 第一个参数必须是程序的相对路径或绝对路径,例如”/bin/ls”或”./a.out”,而不能是”ls”或”a.out”。对于带字母p的函数: 如果参数中包含/,则将其视为路径名。 否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
带有字母l( 表示示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有…,…中的最后一个可变参数应该是NULL, 起sentinel的作用用。
带有字母v( 表示示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就main函数的argv参数或者环境变量表一样。
对于以e (表示示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。
可执行文件查找方式
表中的前四个函数的查找方式都是指定完整的文件目录路劲,而最后两个函数(以p结尾的函数)可以只给出文件名,系统会自动从环境变量”$PATH”所包含的路径中进行查找。
参数表传递方式
两种方式:一个一个列举和将所有参数通过指针数组传递
一函数名的第5个字母按来区分,字母”l”(list)的表示一个一个列举方式;字母”v”(vector)的表示将所有参数构造成指针数组传递,其语法为char *const argv[]
环境变量的使用
exec函数族可以默认使用系统的环境变量,也可以传入指定的环境变量。这里,以”e”(Envirment)结尾的两个函数execle、execve就可以在envp[]中传递当前进程所使用的环境变量。
使用的区别
可执行文件查找方式
参数表传递方式
环境变量的使用
exec函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则
返回-1, 所以exec函数只有出错的返回值而没有成功的返回值。
char *const ps_argv[] ={“ps”, “-o”, “pid,ppid,pgrp,session,tpgid,comm”, NULL};
char *const ps_envp[] ={“PATH=/bin:/usr/bin”, “TERM=console”, NULL};
execl(“/bin/ps”, “ps”, “-o”, “pid,ppid,pgrp,session,tpgid,comm”, NULL);
execv(“/bin/ps”, ps_argv);
execle(“/bin/ps”, “ps”, “-o”, “pid,ppid,pgrp,session,tpgid,comm”, NULL, ps_envp);
execve(“/bin/ps”, ps_argv, ps_envp);
execlp(“ps”, “ps”, “-o”, “pid,ppid,pgrp,session,tpgid,comm”, NULL); execvp(“ps”, ps_argv);
进程的终止:
对于父进程已经终止的所有进程,他们的父进程都改变为init进程。我们称这些进程有
Init领养。一个init的子进程(包括领养进程)终止时,init会调用一个wait函数取得其 终止状态。
对于一个已经终止、但其父进程尚未对其进行善后处理(获取终止子进程的有关信息,
释放它仍占有的资源)的进程被称为僵尸进程。子进程终止时,虽然不在运行,但它仍
然存在与系统中,进程表中代表子进程的表项不会立刻被释放,因为它的退出码还需
要保存在进程表项中以备父进程今后的wait调用使用,也就是说终止子进程与父进程
之间的关联还会保持,直到父进程也正常的终止或父进程调用wait才告结束。
(1)正常终止:
(a)在main函数内执行return语句。这等效于调用exit。
(b)调用exit函数
(c)调用_exit系统调用函数
(2)异常终止:
(a)调用abort。它产生SIGABRT信号,所以是一种异常终止的一种特列。
(b)当进程接收到某个信号时。例如,进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。
注意:不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
_exit()函数的作用最为简单:直接进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O”缓冲。