引子
前文我们讲了fork和COW的原理,本文接着上文续写vfork,clone等方式创建进程原理。
vfork
vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。
vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
也可以这么理解,vfork创建出来的不是真正意义上的进程,而是一个线程。
arduino
#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;
}
结果:
ini
【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一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。先有必要说下这个函数的结构:
arduino
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
其中关键参数:
- child_stack为给子进程分配系统堆栈的指针(在linux下系统堆栈空间是2page大小,就是8K的内存,其中在这块内存中,低地址上存放的值就是进程控制块task_struct的值);
- flags为要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共
参数 | 含义 |
CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了"兄弟"而不是"父子" |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录 |
CLONE_FILES | 子进程与父进程共享相同的fd表 |
CLONE_NEWNS | 在新的namespace启动子进程,namespace描述了进程的文件层级 |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时PID与父进程一致 |
CLONE_THREAD | Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法.而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。
Android下底层创建线程的方式也是用clone的方式实现的,参考如下图:
exec
exec和其他三种创建进程原理上不属于同一个层次,可以理解为是它是为了加载程序而存在,所以这里我们就浅聊下就可以了。一般我们创建子进程都是为了运行新的程序代码,因此Linux系统提供了一个exec()函数族,用于创建和修改子进程。调用exec()函数时,子进程中的代码段、数据段和堆栈段都将被替换。由于调用exec()函数并没有创建新进程,因此修改后的子进程的ID并没有改变。
arduino
//第一组,l->list,p->path,e->envp
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[]);
//第二组,v->vector,p->path,e->envp
int 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->envp
int execve(const char *pathname, char *const argv[], char *const envp[]);
总结
本系列文章因在看Android底层源码时候接触到的一个小知识点引发而来,分上下两篇文章道明了Linux下创建进程的方式并阐明了在Android场景下的使用方式,希望大家能从中解惑,也希望大家能从中学到持续学习,吾日三省吾身的精神,欢迎各位coder关注公众号,第一时间技术交流。