fork系统调用是创建子进程的唯一方法。执行过程如下:
头文件:unistd.h
函数原型:pid_fork(void)
功能:创建一个与原来进程几乎完全相同的进程,即两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来的进程所有值都复制到新的进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
返回值:fork被调用一次却能够返回两次且可能有三种不同的返回值:
其实,fork底层是调用了内核的函数来实现fork的功能的,即先create()先创建进程,此时进程内容为空,然后clone()复制父进程的内容到子进程中,此时子进程就诞生了,接着父进程就return返回了。而子进程诞生后,是直接运行return返回的,然后接着执行后面的程序,这里注意:子进程是不会执行前面父进程已经执行过的程序了得,因为PCB中记录了当前进程运行到哪里,而子进程又是完全拷贝过来的,所以PCB的程序计数器也是和父进程相同的,所以是从fork()后面的程序继续执行。此时就按照前面的规则进行判断返回[1]。如下图所示:
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样[2]。
头文件:unistd.h
函数原型:
int execl(const char *path, const char *arg, …);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg,…, char * const envp[]);
int execve(const char *file, char *const argv[]);
int execlp(const char *file, const char *arg, …);
int execvp(const char *file, char *const argv[]);
选项:
l:希望接收一个以逗号分隔的参数列表,列表以NULL指针作为结束标志;
v:希望接收一个以NULL结尾的字符串数组的指针;
p:是一个以NULL结尾的字符串数组指针,函数可以利用PATH变量查找子程序文件;
e:函数传递指定参数envp,允许改变子进程的环境,无后缀e时子进程使用当前程序的环境。
参数:
path:可执行文件的路径名字;
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束;
file:如果参数file中包含/,就将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
功能:
以新进程代替原有进程,但PID保持不变。
返回值:
执行成功不返回,出错则返回-1,失败原因记录在errno中。
六个函数的区别:
execl 实现ls指令
execv 实现获取系统时间
执行exec系统调用,一般都是这样,用fork()函数新建立一个进程,然后让进程去执行exec调用。我们知道,在fork()建立新进程之后,父进程与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容copy到子进程中去,还有上下文也会copy到子进程中去。而为了提高效率,采用一种写时copy的策略,即创建子进程的时候,并不copy父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。从而父子进程拥有独立的地址空间。而对于fork()之后执行exec后,这种策略能够很好的提高效率,如果一开始就copy,那么exec之后,子进程的数据会被放弃,被新的进程所代替。
exit系统调用的执行发生以下事件:
函数原型1:void _exit(int status)
头文件:unistd.h
函数原型2:void exit(int status)
头文件:stdlib.h
功能:终止发出调用的进程,status是返回给父进程的状态值,父进程可通过wait系统调用获得。
exit()和_exit()的区别:
l _exit()的作用最简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
l exit()在终止进程之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,即清理“I/O缓冲”;
l 两者最终都要将控制权交给内核,旖旎次,要想保证数据的完整性,就一定要使用exit()。
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
头文件:sys/types.h和sys/wait.h
函数原型1:pid_t wait(int *status)
函数原型2:pid_t waitpid(pid_t pid, int *status, int options)
参数:
status:返回子进程退出时的状态;
pid:pid>0时:等待进程号为pid的子进程结束、pid=0时:等待组ID等于调用进程组ID的子进程结束、pid=-1时:等待任一子进程结束,等价于调用wait()、pid<-1时:等待组ID等于PID的绝对值的任一子进程结束;
options:WNOHANG:若pid指定的子进程没有结束,则waitpid()不阻塞而立即返回,此时的返回值为0、WUNTRACED:为了实现某种操作,由pid指定的任一进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态、0:同wait(),阻塞父进程,等待子进程退出。
wait()与waitpid()区别:wait()函数等待所有子进程的僵死状态,waitpid()函数等待PID与参与pid相关的子进程的僵死状态。
检查子进程的返回状态码status:
WIFEXITED(status):进程中通过调用_exit()或exit()正常退出,该宏值为非0;
WIFSIGNALED(status):子进程因得到的信号没有被捕捉导致退出,该宏值为非0;
WIFSTOPPED(status):子进程没有终止但停止了,并可重新执行时,该宏值为非0,这种情况仅出现在waitpid()调用中使用了WUNTRACED选项;
WEXITSTATUS(status):如果WIFEXITED(status)返回非0,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值;
WTERMSIG(status):如果WIFSIGNALED(status)返回非0,该宏返回导致子进程退出的信号的值;
WSTOPSIG(status):如果WIFSTOPPED(status)返回非0,该宏返回导致子进程停止的信号的值。