Linux进程的基本操作:fork vfork exec

进程创建

进入进程的运行状态时,需要首先创建一个新的进程。在Linux系统中,提供了几个关于创建新进程的操作函数,如fork()函数、vfork()函数和exec()函数族等。

1.fork()函数

fork()函数的功能是创建一个新的进程,新进程为当前进程的子进程,那么当前的进程就被称为父进程。在一个函数中,可以通过fork()函数的返回值判断进程是在子进程中还是父进程中。fork()函数的调用形式为:

pid_t fork(void);

使用fork()函数需要引用 sys/types.h 和 unistd.h头文件,该函数的返回值类型为pid_t,表示一个非负整数。若程序运行在父进程中,函数返回的PID为子进程的进程号;若运行在子进程中,返回的PID为0.
如若调用fork()函数创建子进程失败,那么就会返回-1,并且提示错误信息。错误信息有以下两种形式:
EAGAIN:表示fork()函数没有足够的内存用于复制父进程的分页表和进程结构数据。
ENOMEM:表示fork()函数分配必要的内核数据结构时内存不足。

#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
    pid_t pid;
    if((pid = fork()) < 0)
    {
        printf("fork error!\n");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("in the child process!\n");
    }
    else
    {
        printf("in the parrent process!\n");
    }
    exit(0);
}

在实例中,通过fork()函数的返回值确定程序是运行在父进程中还是子进程中。运行结果如下

gcc -g -o fork1 fork1.c
./fork1
in the parrent process!
in the child process!

由程序的结果可以发现fork()函数的一个特点,那就是“调用一次,返回两次”,这样的特点是如何实现的呢?通过下图可以分析其原因。
这里写图片描述
从上图可以看出,在一个程序中,调用到fork()函数后,就出现了分叉。在子进程中,fork()函数返回0;在父进程中,fork()函数返回子进程的ID。因此,fork()函数返回值后,开发人员可以根据返回值的不同,对父进程和子进程执行不同的代码,这样就使得fork()函数具有“调用一次,返回两次”的特点。但是,父进程与子进程的返回顺序并不是固定的,由于fork()函数是系统调用函数,因此取决于系统中其他进程的运行情况和内核的调度算法。

2.vfork()函数

vfork()函数与fork()函数相同,都是系统调用函数,两者的区别是在创建子进程时fork()函数会复制所有父进程的资源,包括进程环境、内存资源等,而vfork()函数在创建子进程时不会复制父进程的所有资源,父子进程共享地址空间。这样,在子进程中对虚拟内存空间中变量的修改,实际上是在修改父进程虚拟内存空间中的值。

#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int gvar = 2;
int main(void)
{
    pid_t pid;
    int var = 5;
    printf("process id:%ld\n",(long)getpid());
    printf("gvar = %d var = %d\n",gvar,var);
    if((pid = vfork()) < 0)
    {
        printf("error!\n");
        return 1;
    }
    else if(pid == 0)
    {
        gvar--;
        var++;
        printf("the child process id:%ld\ngvar = %d var = %d\n",(long)getpid(),gvar,var);
        _exit(0);
    }
    else
    {
        printf("the parrent process id:%ld\ngvar = %d var = %d\n",(long)getpid(),gvar,var);
        return 0;
    }
}

运行结果如下

gcc -g -o vfork2 vfork2.c
./vfork2
process id:10239
gvar = 2 var = 5
the child process id:10240
gvar = 1 var = 6
the parrent process id:10239
gvar = 1 var = 6

3.exec()函数族

通过调用fork()函数和vfork()函数创建子进程,子进程和父进程执行的代码是相同的。但是,通常创建了一个新进程也就是子进程后,目的时要执行与父进程不同的操作,实现不同的功能。因此,Linux系统提供了一个exec()函数族,用于创建和修改子进程。调用exec()函数时,子进程中的代码段、数据段和堆栈段都将被替换。由于调用exec()函数并没有创建新进程,因此修改后的子进程的ID并没有改变。
exec()函数族由6种以exec开头的函数组成,定义形式分别如下:

int execl(const char* path,const char* arg,...);
int execlp(const char* file,const char* arg,...);
int execle(const char* path,const char* arg,...,char* const envp[]);
int execv(const char* path,const char* argv[]);
int execve(const char* path,const char* argv[],char* const envp[]);
int execvp(const char* file,const char* argv[]);

这些函数都定义在系统函数库中,在使用前需要引用头文件sys/types.h 和 unistd.h,并且必须在预定义时定义一个外部的全局变量,例如

extern char** environ;

上面定义的变量是一个指向Linux系统全局变量的指针。定义了这个变量后,就可以在当前工作目录中执行系统程序,如同在shell中不输入路径直接运行vim和Emacs等程序一样。
exec()函数族中的函数都实现了对子进程中的数据段、代码段和堆栈段进行替换的功能,如果调用成功,则加载新的程序,没有返回值。如果调用出错,则返回值为-1。

/*******execve.c文件**********/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
extern char** environ;
int main(int argc,char* argv[])
{
    execve("new",argv,environ);
    puts("this information will not be output in normal situation!");
}
/*************new2.c文件*************/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
    puts("welcome to linux!");
    return 0;
}

运行结果如下:

gcc -o execve execve.c
gcc -o new new2.c
./execve
welcome to linux!

在这个运行结果中,只输出了“welcome to linux!”,说明在execve.c这个程序中执行了new2.c这个程序中的代码,那么execve.c程序中的“this information will not be output in normal situation!”这句话为什么没有正常输出呢?原因就是调用了execve()函数,调用了这个函数后,将进程中的代码段、数据段和堆栈段都进行了修改,使得这个新创建的子进程只执行了新加载的这个程序的代码,此时父进程与子进程的代码不再有任何关系。执行了execve()函数后,原来存在的代码都被释放了,即execve.c这个文件中的puts(“this information will not be output in normal situation!”)代码得不到执行,因此无法输出此信息。

猜你喜欢

转载自blog.csdn.net/u010183728/article/details/81290342