shell system exec fork
启动新进程(system 函数)
system() 函数可以启动一个新的进程。
int system (const char *string)
这个函数的效果就相当于执行 sh –c string。
一般来说,使用 system 函数远非启动其他进程的理想手段,因为它必须用一个 shell 来启动需要的程序。这样对 shell 的安装情况,以及 shell 的版本依赖性很大。
system 函数的特点:
建立独立进程,拥有独立的代码空间,内存空间
等待新的进程执行完毕,system 才返回。(阻塞)
替换进程映像(exec 函数)
exec 函数可以用来替换进程映像。执行 exec 系列函数后,原来的进程将不再执行,新的进程的 PID、PPID 和 nice 值与原先的完全一样。其实执行 exec 系列函数所发生的一切就是,运行中的程序开始执行 exec 调用中指定的新的可执行文件中的代码。
exec 函数的特点:
当进程调用一种 exec 函数时,源进程完全由新程序代换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。特别地,在原进程中已经打开的文件描述符,在新进程中仍将保持打开,除非它们的 “执行时关闭标志”(close on exec flag)被置位。任何在原进程中已打开的目录流都将在新进程中被关闭。
复制进程映像(fork 函数)
fork 函数
头文件
- #include
- #include
- #include
函数原型
- pid_t fork( void);
返回值:
若成功调用一次则返回两个值,子进程返回 0,父进程返回子进程 ID;否则,出错返回 - 1
关于 fork 函数的作用,《Linux 程序设计》中是这样解释的:
我们可以通过调用 fork 创建一个新进程。这个系统调用复制当前进程,在进程表中新建一个新的表项,新表项中的许多属性与当前进程是相同的。新进程几乎与元进程一模一样,执行的代码也完全相同,但是新进程有自己的数据空间、环境和文件描述符。
这个解释其实过于笼统,很多细节问题都没有说。下面就简单说一下调用 fork 时发生的一些细节问题。或者叫 fork 函数的特点:
首先,现在的 UNIX 系统和 Linux 系统都采用写时复制技术(COW:Copy On Write)。使用这种技术,当调用 fork 函数时,新的进程只是拥有自己的虚拟内存空间,而没有自己的物理内存空间。新进程共享源进程的物理内存空间。而且新内存的虚拟内存空间几乎就是源进程虚拟内存空间的一个复制。
我们知道,进程空间可以简单地分为程序段(正文段)、数据段、堆和栈四部分(简单这样理解)。采用写时复制的 fork 函数,当执行完 fork 后的一定时间内,新的进程(子进程)和源进程的进程空间关系如下图:
如上图,fork 执行时,Linux 内核会为新的进程 P2 创建一个虚拟内存空间,而新的虚拟空间中的内容是对 P1 虚拟内存空间中的内容的一个拷贝。而 P2 和 P1 共享原来 P1 的物理内存空间。
当然要理解 “写时复制” 中,上图中所展示的状态是会发生变化的。什么时候回发生变化呢?就是,父子两个进程中任意一个进程对数据段、栈区、堆区进行写操作时,上图中的状态就会被打破,这个时候就会发生物理内存的复制,这也就是叫 “写时复制” 的原因。发生的状态转变如下:
我们发现,P2 有了属于自己的物理内存空间。值得注意的是,各个段之间发生的变化应当是独立的,也就是说,如果只有数据段发生了写操作那么就只有数据段进行写时复制。而堆、栈区域依然是父子进程共享。还有一个需要注意的是,正文段(程序段)不会发生写时复制,这是因为通常情况下程序段是只读的。子进程和父进程从 fork 之后,基本上就是独立运行,互不影响了。
此外需要特别注意的是,父子进程的文件描述符表也会发生写时复制。
还有一个叫 vfork 的函数,这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
system()、exec()、fork() 函数比较
首先比较一下 exec() 函数和 fork()。这两个函数一个是换药不换汤(execl 函数),另一个是换汤不换药(fork 函数)。那么什么是汤、什么又是药呢?我们知道进程是个很复杂的东西。从 task_struct 结构体的代码量上就可以看出来(task_struct 是 Linux 内核中用来描述进程的一个结构体,这个结构体光代码貌似就有好几屏)。我们可以把进程的 PID、PPID 和 nice 值等看作是汤,而把进程空间(简单理解就是正文段、数据段、堆、栈等)看作是药。
exec() 函数是换药不换汤,就是说执行 exec 函数后,并没有产生新的进程,也就是汤还是那些汤,进程的 PID、PPID 和 nice 值等没有发生变化。但是 exec() 函数却将药换了,也就是将进程空间换掉了,新的进程空间是为了执行新的程序所准备的,所以新的进程空间与原进程空间并没有什么关系。
fork() 函数是换汤不换药,意思是执行 fork() 函数后,产生了新的进程,新的进程的 PID、PPID 与原来原来的进程不同,说明父子进程是两个不同的进程,但是 fork 并没有把药换掉,而是将药复制了一份给子进程。fork 刚执行后的一段时间内,父子进程有着相同的状态(进程空间中的东西都一样,因为 fork 采用 “写时复制”,一开始父子进程共享物理内存空间)。但是一旦父子进程中有一个进程试图修改进程空间,这时父子进程就各自拥有了各自的进程空间,简单地理解,从这一时刻器,父子进程就是两个独立的进程,谁都不会影响谁(实际上还是有一定影响的,在这里可以忽略),父子进程之间的关联仅剩下它们共享的代码段了。
对于 system 函数,我们可以先看一下它的源代码:
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
我们看到 system() 函数实际上就是先执行了 fork 函数,然后新产生的子进程立刻执行了 exec 函数,我们前面说个 fork 函数换汤不换药,exec 函数换药不换汤,那么 system 函数就是既换汤也换了药,也就是 system 函数会产生新进程,这就意味着新进程的 PID、PPID 等与原进程不同。system 也会产生新的进程空间,而且新的进程空间是为新的程序准备的,所以和原进程的进程空间没有任何关系(不像 fork 新进程空间是对原进程空间的一个复制)。还要注意的是,system 函数代码中 else 部分执行了 wait 函数,这就意味着,原进程会等待子进程执行完毕(阻塞)
最后还要注意的一个问题是关于文件描述符的。
exec 函数执行后,原来打开的文件描述符依然存在。
fork 函数执行后,原来打开的文件描述符会复制一份到新的进程中,之后两个进程之间的文件描述符就相对独立了。
system 函数先执行 fork 函数,这之后两个进程的文件描述符就相对独立了。之后 exec 函数并不影响文件描述符。