vfork 与进程创建
进程创建回顾
int create_process(char *path, char *args[], char *env[])
{
int ret = fork();
if (ret == 0) {
execve(path, args, env);
}
return ret;
}
问题:进程创建是否只能依赖于 fork() 和 execve() 函数?
再论进程创建
fork()
通过完整复制当前进程的方式创建新进程execve()
根据参数覆盖进程数据(一个不留)
pid_t vfork(void);
vfork()
用于创建子进程,然而不会复制父进程空间中的数据vfork()
创建的子进程直接使用父进程空间(没有完整独立的进程空间)vfork()
创建的子进程对数据(变量)的修改会直接反馈到父进程中vfork()
是为了 execve() 系统调用而设计
下面的程序运行后会发生什么?
vfork.c
#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;
}
printf("parent = %d, var = %d\n", getpid(), var);
return 0;
}
parent = 52385
pid = 52386, var = 88
pid = 52386, var = 89
parent = 52385, var = -661685664
Segmentation fault (core dumped)
vfork() 深度分析
- 子进程使用父进程的数据空间
vfork() 要点分析
- vfork() 成功后,
父进程将等待子进程结束
- 子进程可以使用父进程的数据(堆,栈,全局)
子进程可以从创建点调用其它函数,但不要从创建点返回
- 当 子进程执行流 回到创建点 / 需要结束 时,使用
_exit(0)
系统调用 - 如果使用 return 0 那么将破坏栈结构,导致后续父进程执行出错
- 当 子进程执行流 回到创建点 / 需要结束 时,使用
当子进程调用其它函数及被调用函数返回时,不会破坏原有的栈空间
在上述代码中,子进程使用 return
结束自己时导致了栈回收。当子进程结束,父进程开始运行时,栈帧已经被销毁,出现运行错误。
#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; /* distroy parent stack frame */
_exit(0);
}
printf("parent = %d, var = %d\n", getpid(), var);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out
parent = 52539
pid = 52540, var = 88
pid = 52540, var = 89
parent = 52539, var = 89
fork 与 vfork 的选择
个人思考:vfork 弊大于利,使用时需要格外小心避免奇奇怪怪的问题,fork 虽然效率低,但更容易被使用
fork() 的现代优化
Copy-on-Write 技术
- 多个任务访问同一资源,在写入操作修改资源时,复制资源的原始副本
fork()
引入 Copy-on-Write 之后,父子进程共享相同的进程空间- 当父进程或子进程的其中之一修改内存数据,则实时复制进程空间
fork() + execve() ←→ vfork() + execve()
Linux的fork()系统调用会创建一个新的进程,但是该进程与父进程共享相同的内存映射。当进程调用exec()函数时,该进程的内存映射会被替换为新的程序的内存映射,但是这个操作并不会导致进程的复制。实际上,写时复制(Copy-on-Write)机制会延迟对共享内存的复制,直到其中一个进程试图对共享内存进行写操作时才会进行复制。这样可以减少内存的使用和复制的开销。
fork出来子进程之后,父子进程哪个先调度直接决定了是否需要拷贝的问题?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,而避免无用的复制。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度。
编程实验 fork & vfork
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.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;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out ./helloword.out
current : 54739
Hello Word!
result = 0
exec 与 system 简介
exec 函数家族
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *file, char *const argv[], char *const envp[]);
l(list):参数地址列表,以空指针结尾
v(vector):存有各参数地址的指针数组的地址
p(path):按 PATH 环境变量指定的目录搜索可执行文件。
e(environment):存有环境变量字符串地址的指针数组的地址。
exec 函数族装入并运行可执行程序 path/file,并将参数 arg0 ( arg1, arg2, argv[], envp[] ) 传递给此程序。
exec 函数族与一般的函数不同,exec 函数族中的函数执行成功后不会返回,而且,exec 函数族下面的代码执行不到。只有调用失败了,它们才会返回 -1,失败后从原程序的调用点接着往下执行。
path:要执行的程序路径。可以是绝对路径或者是相对路径。在execv、execve、execl和execle这4个函数中,使用带路径名的文件名作为参数。
file:要执行的程序名称。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件。
argv:命令行参数的矢量数组。
envp:带有该参数的exec函数可以在调用时指定一个环境变量数组。其他不带该参数的exec函数则使用调用进程的环境变量。
arg:程序的第0个参数,即程序名自身。相当于argv[O]。
…:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后应该增加一个空的参数项(NULL),表明命令行参数结束。
返回值:一1表明调用exec失败,无返回表明调用成功。
函数 | 使用文件名 | 使用路径名 | 使用参数列表(函数出现字母l) | 使用 argv(函数出现字母v) | 指定环境变量 |
execl | √ | √ | |||
execlp | √ | √ | |||
execle | √ | √ | √ | ||
execv | √ | √ | |||
execvp | √ | √ | |||
execve | √ | √ | √ |
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
char pids[32] = {0};
char* const ps_argv[] = {"pstree", "-A", "-p", "-s", pids, NULL};
char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Delphi", NULL};
sprintf(pids, "%d", getpid());
execl("/bin/pstree", "pstree", "-A", "-p", "-s", pids, NULL);
execlp("pstree", "pstree", "-A", "-p", "-s", pids, NULL);
execle("/bin/pstree", "pstree", "-A", "-p", "-s", pids, NULL, ps_envp);
execv("/bin/pstree", ps_argv);
execvp("pstree", ps_argv);
execve("/bin/pstree", ps_argv, ps_envp);
return 0;
}
进程创建库函数
#include <stdlib.h>
int system(const char *command);
- 参数, 程序名及进程参数 (如:pstree -A -p -s $$)
- 返回值,进程退出状态
system 在 linux 中的实现(system 首先创建 shell 进程,功能强大,但效率较低)
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。
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.sh
echo "Hello world from shell"
a=1
b=1
c=$(($a + $b))
echo "c = $c"
system.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int result = 0;
printf("current : %d\n", getpid());
result = system("pstree -A -p -s $$"); // $$ shell 标识,表示当前进程 pid
printf("result : %d\n", result);
result = system("./system.sh");
printf("result : %d\n", result);
return 0;
}
tiansong@tiansong:~/Desktop/linux$ chmod 777 ./system.sh
tiansong@tiansong:~/Desktop/linux$ ./system.sh
Hello world from shell
c = 2
tiansong@tiansong:~/Desktop/linux$ gcc system.c
tiansong@tiansong:~/Desktop/linux$ ./a.out
current : 55370
systemd(1)---sh(2545)---node(2555)---node(2738)---bash(12984)---a.out(55370)---pstree(55371)
result : 0
Hello world from shell
c = 2
result : 0
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。