写点什么

Android 下 Linux 创建进程的姿势(下)

作者:江湖修行
  • 2023-11-16
    北京
  • 本文字数:2421 字

    阅读完需:约 8 分钟

Android下Linux创建进程的姿势(下)

引子

前文我们讲了 fork 和 COW 的原理,本文接着上文续写 vfork,clone 等方式创建进程原理。

vfork

vfork 也是创建一个子进程,但是子进程共享父进程的空间。在 vfork 创建子进程之后,父进程阻塞,直到子进程执行了 exec()或者 exit()。vfork 最初是因为 fork 没有实现 COW 机制,而很多情况下 fork 之后会紧接着 exec,而 exec 的执行相当于之前 fork 复制的空间全部变成了无用功,所以设计了 vfork。


vfork 和 fork 之间的另一个区别是:vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。


也可以这么理解,vfork 创建出来的不是真正意义上的进程,而是一个线程。

#include<unistd.h>#include<stdio.h>#include<stdlib.h>int main(int argc, char*argv[]) {    pid_t ret;    int count = 0;    //在父进程的空间中,定义一个count 共享变量    printf("【parent】 shared count=%p in pid=%d\n", & count, getpid());    printf("【parent】fork in pid=%d\n", getpid());    ret = vfork(); //vfork 父子进程共享对象count,父、子进程共享的count 变量其虚拟内存地址一致,在调用vfork之后父进程会挂起,子进程对count 修改会体现在父进程中    if (ret == 0) {        printf("【child】start from pid=%d\n", getpid());        count = 10;        printf("【child】assign count=%p ,count=%d\n", & count, count);        sleep(2);        _exit(0);//退出子进程,必须调用,因为使用vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数退出,否则会报vfork: cxa_atexit.c:100: __new_exitfn: Assertion `l != ((void *)0)' failed    } else {        printf("【parent】continue in parent pid=%d\n", getpid());        printf("【parent】ret=%d, &count=%p , count=%d\n", ret, & count, count);        printf("【parent】pid=%d\n", getpid());    }    return 0;}
复制代码


结果:


【parent】shared count=0x7ffe774fe418 in pid=7950【parent】fork in pid=7950【child】start from pid=7951【child】assign count=0x7ffe774fe418,count=10 //这里会sleep(2) 然后 父进程才会继续执行【parent】continue in parent pid=7950【parent】ret=7951, &count=0x7ffe774fe418 , count=10【parent】the pid=7950
复制代码


我搜了下 android 的源码工程,发现用 vfork 的地方也寥寥无几,在 recovery 的升级模块有用到。文件路径:/bootable/recovery/updater/install.cpp。

clone

clone 是 Linux 为创建线程设计的,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。


系统调用 fork()和 vfork()是无参数的,而 clone()则带有参数。fork()是全部复制,vfork()是共享内存,而 clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags 决决定。


clone 函数功能强大,带了众多参数,它提供了一个非常灵活自由的创建进程的方法。因此由他创建的进程要比前面 2 种方法要复杂。clone 可以让你有选择性的继承父进程的资源,你可以选择像 vfork 一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。先有必要说下这个函数的结构:


int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
复制代码


其中关键参数:


  • child_stack 为给子进程分配系统堆栈的指针(在 linux 下系统堆栈空间是 2page 大小,就是 8K 的内存,其中在这块内存中,低地址上存放的值就是进程控制块 task_struct 的值);

  • flags 为要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共



fork 不对父子进程的执行次序进行任何限制,fork 返回后,子进程和父进程都从调用 fork 函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法.而在 vfork 调用中,子进程先运行,父进程挂起,直到子进程调用了 exec 或 exit 之后,父子进程的执行次序才不再有限制;clone 中由标志 CLONE_VFORK 来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。


Android 下底层创建线程的方式也是用 clone 的方式实现的,参考如下图:


exec

exec 和其他三种创建进程原理上不属于同一个层次,可以理解为是它是为了加载程序而存在,所以这里我们就浅聊下就可以了。一般我们创建子进程都是为了运行新的程序代码,因此 Linux 系统提供了一个 exec()函数族,用于创建和修改子进程。调用 exec()函数时,子进程中的代码段、数据段和堆栈段都将被替换。由于调用 exec()函数并没有创建新进程,因此修改后的子进程的 ID 并没有改变。


//第一组,l->list,p->path,e->envpint 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[]);//第二组,v->vector,p->path,e->envpint execv(const char *pathname, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);//第三组,v->vector,e->envpint execve(const char *pathname, char *const argv[], char *const envp[]);
复制代码

总结

本系列文章因在看 Android 底层源码时候接触到的一个小知识点引发而来,分上下两篇文章道明了 Linux 下创建进程的方式并阐明了在 Android 场景下的使用方式,希望大家能从中解惑,也希望大家能从中学到持续学习,吾日三省吾身的精神,欢迎各位coder关注公众号,第一时间技术交流。

发布于: 5 小时前阅读数: 10
用户头像

江湖修行

关注

还未添加个人签名 2021-12-05 加入

还未添加个人简介

评论

发布
暂无评论
Android下Linux创建进程的姿势(下)_android_江湖修行_InfoQ写作社区