一、进程标识
每个进程都有一个非负整型表示的唯一进程ID。虽然该id是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程id就成为复用的候选者。
系统中有一些专用进程,但是具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被成为系统进程。进程ID 1通常是init进程,在自举过程结束时有内核调用。init进程绝不会终止,他是一个普通用户的用户进程(与交换进程不同,他不是内核中的系统进程),但是它以超级用户权限运行。
每个unix系统实现都有它自己的一套提供操作系统服务的内核进程,比如,在某些unix的虚拟存储器中,进程id 2是页守护进程(page daemnon),此进程负责支持虚拟存储系统的分页操作。
附:进程相关函数
1 #include <sys/types.h> 2 #include <unistd.h> 3 pid_t getpid(void); 返回:调用进程的进程 I D 4 pid_t getppid(void); 返回:调用进程的父进程 I D 5 uid_t getuid(void); 返回:调用进程的实际用户 I D 6 uid_t geteuid(void); 返回:调用进程的有效用户 I D 7 gid_t getgid(void); 返回:调用进程的实际组 I D 8 gid_t getegid(void); 返回:调用进程的有效组 I D
二、fork函数
#include<unistd.h> pid_t fork(void);
解释:有fork函数创建的新进程成为子进程,fork函数被调用一次,但返回两次,两次返回的区别是子进程返回值是0,父进程的返回值则是新建子进程的进程id。子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本。例如,子进程获取父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分。该部分执行写时复制。
1 #include<sys/types.h> 2 #include "ourhdr.h" 3 int glob=6; 4 char buf[]="a write to stdout\n"; 5 int main(void) 6 { 7 8 int var; 9 pid_t pid; 10 var=88; 11 if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1) 12 printf("write error"); 13 printf("before fork\n"); 14 if((pid=fork())<0) 15 printf("fork error\n"); 16 else if(pid==0) 17 { 18 glob++; 19 var++; 20 } 21 else 22 sleep(2); 23 printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var); 24 exit(0); 25 }
可以看到子进程对变量所做的改变并不影响父进程中该变量的值。
一般来讲,在fork之后是父进程还是子进程先执行是不确定的,这取决于内核所使用的调度算法,如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
文件共享
在fork之后处理文件描述符有以下两种常见的情况:
(1)父进程等待子进程 完成。(2)父进程和子进程各自执行不同的程序段。
除了打开文件之外,父进程的很多其他属性也有子进程继承,包括:
实际用户I D、实际组I D、有效用户I D、有效组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 设置-用户- I D标志和设置-组- I D标志。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。
父、子进程之间的区别是:
• fork的返回值。
• 进程I D。
• 不同的父进程I D。
• 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集。
fork失败的两个原因:
(a)系统中已经有了太多的进程(通常意味着某个方面出了问题)
(b)该实际用户id的进程总数超过了系统限制。
fork的用法:
(1)一个父进程希望赋值自己,是父进程和子进程同时执行不同的代码段。
(2)一个进程要执行一个不同的程序。
函数vfork
(1)vfork函数用于创建一个新的进程,而该进程的目的是exec一个新程序。vfork与fork的区别:(1)v f o r k与f o r k一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 e x e c (或e x i t ),于是也就不会存访该地址空间。不过在子进程调用 e x e c或e x i t之前,它在父进程的空间中运行。这种工作方式在某些 U N I X的页式虚存实现中提高了效率(与上节中提及的,在 f o r k之后跟随e x e c,并采用在写时复制技术相类似)。
(2)v f o r k保证子进程先运行,在它调用 e x e c或e x i t之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)
函数exit
进程有5种正常终止和3种异常终止方式
5种正常终止方式为:
在main函数内执行return语句,调用exit函数,调用_exit函数或者_Exit函数,进程的最后一个线程在其启动例程中执行return语句,进程的最后一个线程调用pthread_exit函数。
3种异常终止方式如下:
调用abort,当进程接收到某些信号时,最后一个线程对“取消”请求做出响应。
在任意一种终止情况下,该终止进程的父进程都可以用wait或waitpid函数取得其终止状态。
对于父进程已经终止的所有进程,它们的父进程都改变为init进程,我们称这些进程由init进程收养。
僵死进程:一个已经终止、但是其父进程尚未对其进行善后处理的进程成为僵死进程。
函数wait和waitpid
#include<sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid,int *statloc,int options);
调用wait和waitpid的进程会发生什么
• 阻塞(如果其所有子进程都还在运行)。
• 带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态 )。
• 出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
• 在一个子进程终止前, wait 使其调用者阻塞,而 waitpid 有一选择项,可使调用者不阻
塞。
• waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。
检查w a i t和w a i t p i d所返回的终止状态的宏
对于waitpid函数中pid参数的作用的解释如下:
• pid == -1 等待任一子进程。于是在这一功能方面 w a i t p i d与w a i t等效。
• pid > 0 等待其进程I D与p i d相等的子进程。
• pid == 0 等待其组I D等于调用进程的组I D的任一子进程。
• pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
对于waitpid函数中options参数的作用的解释如下:
options或者是0或者是表中常量按位或者运算的结果。
waitpid函数提供了wait函数没有提供的三个功能:
(1) waitpid等待一个特定的进程 (而wait则返回任一终止子进程的状态 )。
(2) waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
(3) waitpid支持作业控制(以WUNTRACED选择项)。
1 #include<sys/wait.h> 2 #include<unistd.h> 3 #include<stdio.h> 4 #include<stdlib.h> 5 int main(void) 6 { 7 pid_t pid; 8 if((pid=fork())<0) 9 { 10 perror("fork"); 11 }else if(pid==0) 12 { 13 if((pid=fork())<0) 14 perror("fork"); 15 else if(pid>0) 16 exit(0); 17 sleep(2); 18 printf("second child,parent pid is:%d\n",(long)getppid()); 19 exit(0); 20 } 21 if(waitpid(pid,NULL,0)!=pid) 22 perror("waitpid"); 23 exit(0); 24 } 25 ~
以上程序实现的功能是:一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态知道父进程终止,通过调用fork两次来实现。
在第二个子进程中调用 sleep以保证在打印父进程 ID时第一个子进程已终止。在 fork之后,父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程 ID将是创建它的父进程,而不是init进程。