进程创建
进入进程的运行状态时,需要首先创建一个新的进程。在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!”)代码得不到执行,因此无法输出此信息。