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() 根据参数覆盖进程数据(一个不留)

image.png

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() 深度分析

  • 子进程使用父进程的数据空间
    image.png

vfork() 要点分析

  • vfork() 成功后,父进程将等待子进程结束
  • 子进程可以使用父进程的数据(堆,栈,全局)
  • 子进程可以从创建点调用其它函数,但不要从创建点返回

    • 当 子进程执行流 回到创建点 / 需要结束 时,使用 _exit(0) 系统调用
    • 如果使用 return 0 那么将破坏栈结构,导致后续父进程执行出错

当子进程调用其它函数及被调用函数返回时,不会破坏原有的栈空间

image.png

在上述代码中,子进程使用 return 结束自己时导致了栈回收。当子进程结束,父进程开始运行时,栈帧已经被销毁,出现运行错误。

image.png

#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;
}

image.png

编程实验

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

TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧