Linux操作系统学习_用户进程之fork()与exec函数族篇
环境:ubuntu12.04 LTS
1、fork()
由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。这是我们所熟知的过程,但是为什么会产生这样的结果?
将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID。(PS:进程ID0总是由内核交换进程使用,所以一个子进程的进程ID不可能是0)
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。子进程获得父进程数据空间、堆和栈的副本。
由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读。如果父、子进程中的任何一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本。
wait()函数也是进程创建过程中经常使用到的函数,当某一个进程调用了wait函数,该进程就立刻阻塞自己,由wait函数自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回,继续执行;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。因此wait函数常常被用来处理进程间的同步问题。所谓进程同步,即是指协调多个进程的执行次序。关于wait函数更详细的阐述,参看Linux wait()函数这篇文章。
下面是fork()函数的一个例子:
该函数的执行结果如下:
下面对父、子进程之间的写时复制技术进行验证,具体程序代码如下:
可以看到,在父进程创建子进程之前,将父进程中的变量ptmp赋值为58,在fork之后,如果父子进程共享同一块内存区域,而且子进程先于父进程执行,可想而知,在子进程中对ptmp的值进行修改之后,父进程中输出的ptmp的值应该为子进程修改之后的值,若是在子进程要对内存区域进行写操作时,是将该内存区域复制到一处新的内存区域,则子进程修改的只是其单独拥有的那块内存副本之中的ptmp变量,父进程的ptmp变量的值并没有改变。下面,我们来看看这个程序的执行结果:
可以看到父、子进程输出的ptmp的值并不相同,父进程输出的是修改之前的,子进程输出的是修改之后的,所以间接的验证了在fork过程当中的写时复制技术。
2、exec函数族
上面曾提及用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用的exec并不创建新的进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程复制给其子进程的当前进程的正文、数据、堆和栈。
有6种不同的exec函数可供使用,它们常常被统称为exec函数族。这些exec函数使得进程控制原语更加完善。用fork可以创建新进程,用exec函数可以执行新程序。exit函数和两个wait函数处理终止和等待终止。
(1)int execl(const char *path, const char *arg, ......);
(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);
(3)int execv(const char *path, char *const argv[]);
(4)int execve(const char *filename, char *const argv[], char *const envp[]);
(5)int execvp(const char *file, char * const argv[]);
(6)int execlp(const char *file, const char *arg, ......);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
这里主要以execve为例来介绍exec函数族的作用和用法。execve主要有三个主要的参数:1、const char *pathname;2、char *const argv[];3、char *const envp[]。可以看出,第一个参数是命令所在的路径,如/bin/ls,第二个参数主要是命令的集合,如例子中可以看到的"ls"、"-al"等等,通常argv[0]中存储的为即为命令。第三个参数为传递给执行文件的环境变量集。下面给出execve函数的具体在程序中的用法:
可以看到,在fork创建完子进程之后,通过调用execve函数来使子进程执行ls -al /home/vampirem/Cyuyan/fork这一命令,程序的执行结果如下: