进程创建回顾
通过 fork() 创建子进程,然后通过 execve(...) 将子进程的进程空间替代为 path 所指定程序的进程空间,随后执行 path 所指定的程序
问题
进程创建是否只能依赖于 fork() 和 execve(...) ?
再轮进程创建
fork() 通过完整复制当前进程的方式创建新进程,execve() 根据参数覆盖进程数据 (一个不留)
我们在 fork() 之后,立即执行 execve() 的话,fork() 复制父进程进程空间的操作是多余的,因为 execve() 会覆盖复制出来的父进程进程空间
pid_t vfork(void);
vfork() 用于创建子进程,然而不会复制父进程中的数据
vfork() 创建出的子进程直接使用父进程空间 (没有完整独立的进程空间)
vfork() 创建的子进程对数据 (变量) 的修改会直接反馈到父进程中
vfork() 是为了 execve() 系统调用而设计
下面的程序运行后会发生什么?
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
pid_t pid = 0;
int var = 88;
printf("parent = %d\n", getpid());
if( (pid = vfork()) < 0 )
{
printf("vfork error\n");
}
else if( pid == 0 )
{
printf("pid = %d, var = %d\n", getpid(), var);
var++;
printf("pid = %d, var = %d\n", getpid(), var);
return 0; /* destroy parent stack frame */
}
printf("parent = %d, var = %d\n", getpid(), var);
return 0;
}
程序运行结果如下图所示:
程序出现了段错误,崩溃了
程序崩溃是因为 vfork() 后,子进程和父进程共享同一个进程空间,第 26 行,子进程 return 0 之后,会从创建点返回,破坏栈结构,使得父进程的栈空间被破坏掉,从而导致程序崩溃
vfork() 深度解析
vfork() 使得子进程共享父进程的代码和数据
vfork() 要点分析
vfork() 成功后,父进程将等待子进程结束
子进程可以使用父进程的数据 (堆,栈,全局)
子进程可以从创建点调用其他函数,但不要从创建点返回
- 当 子进程执行流 回到创建点 / 需要结束 时,使用 _exit(0) 系统调用
- 如果使用 return 0 那么将破坏栈结构,导致后续父进程执行出错
vfork() 的正确使用
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
pid_t pid = 0;
int var = 88;
printf("parent = %d\n", getpid());
if( (pid = vfork()) < 0 )
{
printf("vfork error\n");
}
else if( pid == 0 )
{
printf("pid = %d, var = %d\n", getpid(), var);
var++;
printf("pid = %d, var = %d\n", getpid(), var);
// return 0; /* destroy parent stack frame */
_exit(0);
}
printf("parent = %d, var = %d\n", getpid(), var);
return 0;
}
vfork() 后,子进程共享父进程的地址空间,父进程会等待子进程运行结束后,再向下运行
第 23 行,将 var 的值加一,此时 var = 89,这个变量的改动会影响到父进程,所以父进程中的 var = 89
第 27 行,_exit(0),使得子进程结束运行,并且不会从创建点返回
程序运行结果如下图所示:
vfork() 虽然可以节约拷贝父进程进程空间的时间,但需要谨慎使用,使用不当会造成程序崩溃
当今的 fork() 实现了读时共享,写时复制的功能,所以如果在 fork() 之后马上调用 execve() 也不会多一次拷贝父进程进程空间的时间,此时的效率是和 vfork() 一样的,这种情况下可以用 fork() 取代 vfork(),程序也会更加安全
fork() 的现代化技术
Copy-on-Write
- 多个任务访问同一资源,在写入操作修改资源时,复制资源的原始副本
fork() 引入 Copy-on-Write 之后,父子进程共享相同的地址空间
- 当父进程或子进程的其中之一修改内存数据,则实时复制进程空间
- fork() + execve() <=> vfork() + execve()
exec 家族函数
create_process 的功能增强
hellocworld.c
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
main.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int create_process(char* path, char* const args[], char* const env[], int wait)
{
int ret = fork();
if( ret == 0 )
{
if( execve(path, args, env) == -1 )
{
exit(-1);
}
}
if( wait && ret )
{
waitpid(ret, &ret, 0);
}
return ret;
}
int main(int argc, char* argv[])
{
char* target = argv[1];
char* const ps_argv[] = {target, NULL};
char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Delphi", NULL};
int result = 0;
if( argc < 2 ) exit(-1);
printf("current: %d\n", getpid());
result = create_process(target, ps_argv, ps_envp, 1);
printf("result = %d\n", result);
return 0;
}
create_process() 函数新增一个参数来决定父进程是否等待子进程运行结束
第 40 行,传入的 wait 参数为 1,父进程会等待子进程运行结束
程序运行结果如下图所示:
进程创建库函数
#include <stdlib.h>
int system(const char* command);
- 参数 -- 程序名及进程参数 (如:pstree -A -p -s $$)
- 返回值 -- 进程退出状态值
system() 函数首先会调用 fork() 函数,父进程调用 waitpid(...) 等待子进程结束,子进程通过 execve(...) 来执行 shell 程序,shell 会解析要执行的命令,来运行程序或者脚本
create_process(...) 和 system(...) 的对比
helloworld.c
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
test.sh
echo "Hello world from shell ..."
a=1
b=1
c=$(($a+$b))
echo "c = $c"
main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int create_process(char* path, char* const args[], char* const env[], int wait)
{
int ret = fork();
if( ret == 0 )
{
if( execve(path, args, env) == -1 )
{
exit(-1);
}
}
if( wait && ret )
{
waitpid(ret, &ret, 0);
}
return ret;
}
int main(int argc, char* argv[])
{
char* target = argv[1];
char* const ps_argv[] = {target, NULL};
char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Delphi", NULL};
int result = 0;
if( argc < 2 ) exit(-1);
printf("current: %d\n", getpid());
result = system(target);
// result = create_process(target, ps_argv, ps_envp, 1);
printf("result = %d\n", result);
return 0;
}
我们使用分别使用 system(...) 函数和 create_process(...) 来运行 helloworld 程序 和 test.sh 脚本
使用 system(...) 函数:
使用 create_process(...) 函数:
通过打印可以看出,我们自己封装的 create_process(...) 函数只能运行可执行程序,而运行不了脚本;而系统库函数 system(...) 既可以运行可执行程序又可以运行脚本,这是因为 system(...) 函数会运行一个 shell 来执行命令
我们如果只想运行可执行程序,可以使用 create_process(...) 函数,这样的效率会更高,因为 system(...) 首先会运行 shell 程序,然后再通过 shell 来执行命令
想要运行脚本,就只能使用 system(...) 函数