Android NDK ——Linux 创建应用进程之 fork vs vfork 小结

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android NDK ——Linux 创建应用进程之 fork vs vfork 小结相关的知识,希望对你有一定的参考价值。

文章大纲

引言

Unix 系列的进程都是通过复制init进程或内核进程而得到的子进程,不同的实现具体细节有所不同其中Linux提供三种fork、vfork、clone 系统调用。

一、Unix 进程概述

在Unix中CPU是以进程为分配单元进行资源分配和调度的,每一个进程都有一个非负整形表示的进程标识符,进程ID总是唯一的,但进程ID是可以重用的,当一个进程终止后其进程ID就可以被其他进程再次使用了,一个普通进程有且只有一个父进程,系统中有一些专用的进程。

  • 0号进程(进程ID为0)是调度进程,又被称为交换进程(swapper),隶属内核的一部分,并不执行任何磁盘上的程序,统一称之为系统进程
  • 1 号进程(进程ID为1)又称为init进程,在系统启动时由内核通过相关的初始化脚本(*.rc 或 init.d等文件)创建并启动,init进程最终会成为所有孤儿进程的父进程。
  • 2号进程(进程ID为2)是页守护进程,负责支持虚拟存储系统的分页操作。

进程除了进程ID还有一些其他的标识,如下表所示(包含不限于)用户进程控制相关的:

获取进程ID标识符的系统调用说明
pid_t getpid(void)调用进程的进程ID
pid_t getppid(void)调用进程的父进程ID
pid_t getuid(void)调用进程的实际用户ID
pid_t geteuid(void)调用进程的有效用户ID
pid_t getgid(void)调用进程的实际用户ID
pid_t getegid(void)调用进程的有效用户ID

通常父进程的很多属性会被子进程锁继承包括(不限于):

  • 实际用户ID、实际组ID、有效用户ID、有效组ID、附加组ID
  • 进程组ID、会话ID、控制终端
  • 设置用户ID标志和设置组ID标志
  • 当前工作目录、根目录、
  • 文件模式创建屏蔽字
  • 针对任一打开文件描述符的在执行时关闭标志(close-on-exec)
  • 环境上下文和连接的共享存储段
  • 存储映射

它们之间主要的区别有:

  • fork 调用后的返回值
  • 进程ID 和进程的PPID不同
  • 子进程的tms_utime、tms_stime、tms_cutime、tms_ustime皆设置为0。
  • 父进程设置的文件锁不会被子进程继承

SIGCHLD——在一个进程终止或者停止时,将SIGCHLD 信号发送给其父进程,系统默认忽略此信号不进行处理,但如果父进程希望被告知子进程的终止或者停止状态,父进程可以监听捕获改信号。

二、fork、vfork、clone

fork的主要应用有:

  • 一个父进程期望通过拷贝自己,使得父、子进程能同时执行不同的代码段,比如网络通信中,父进程等待客户端的请求,当接到请求时,执行fork 使得子进程去处理这个请求,而父进程则继续等待下一个请求。
  • 一个进程要执行另一个不同的程序,比如shell 命令,子进程从fork 返回后立即调用exec 。
#include <uistd.h>

pid_t fork(void)

由fork 创建的新进程称为子进程,fork函数虽然只会执行一次,但是返回两次:

  • 子进程的返回值是0,即可以通过返回值去判断执行的是子进程还是父进程,一个进程有且只有一个父进程(内核交换进程ID始终为0)
  • 父进程的返回值是子进程的pid,因为一个进程的子进程可能有很多个,如果没有告诉给父进程,父进程就无法得知自己子进程的pid到底是多少。

返回之后,父、子进程继续执行fork 调用后的指令,因为fork后子进程获取到的是父进程的数据空间、堆和栈的副本,并不是直接共享这些空间,而是仅仅共享正文段(segment),在Linux下我们可以调用以下三个系统调用来创建子进程。

注意:fork之后父进程和子进程的执行顺序是不确定的,这取决于内核的调度算法。

系统调用说明
fork创建的子进程是父进程的完整副本,即拷贝了父进程的内存空间,包括父进程的数据空间、堆和栈的副本。即父、子进程并不共享这些存储空间,但共享正文段。
vfork创建的子进程与父进程共享数据段,而且vfork()调用后会阻塞当前进程,直到子进程退出,父进程才会继续往下执行。
clone创建的子进程可以由用户根据自己的需求选择性的完全继承或者部分继承父进程的内存空间,相当于是fork的泛型实现,即允许调用者自主控制那些部分由父、子进程共享。

不同的线程库fork的实现略有不同,其他的vfork、clone功能可以看成是fork的扩展版,vfork 和fork 的系统调用差异仅在于clone_flags不一致。

传统的复制肯定会消耗大量的资源,因此Linux 设计了写时复制(Copy-on-write)的策略,其核心思想是父进程和子进程共享页帧而不是复制页帧。因为只要页帧被共享,它们就不能被修改,即页帧被保护。因此无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写,这样原来的页帧仍然是写保护的,即当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝,因此会拥有与父进程相同的物理页面。为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面。同时将父进程A对这些物理页面的访问权限也设成只读。这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常。do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去。

三、vfork 简单测试

vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程

vfork 被调用之后,父进程将会挂起直到子进程结束(exit)和execve(2),在此之前父、子进程共享内存页。

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

/**
* forkdemo.c
* n success ,the PID of the child process is returned int the parent,and 0 is returned
* in the child .
* onFailure,-1 is returned in the parent,no child process is created.and errno is set appropriately.
*
*/
int main(int argc, char* argv[])

    pid_t ret;
    int count =0;
	//在父进程的空间中,定义一个count 共享变量
	printf("【parent】assign shared var on &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 in pid=%d\\n",getpid());
        count=100;
        printf("【child】assign on &count=%p  with count=%d\\n",&count,count);
        sleep(2);
		_exit(0);//退出子进程,必须调用,因为使用vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数退出,否则会报vfork: cxa_atexit.c:100: __new_exitfn: Assertion `l != ((void *)0)' failed
        //execl("./vfork2",0);
    
    else
    
		printf("【parent】continue in parent pid=%d\\n",getpid());
        printf("【parent】ret=%d, &count=%p , count=%d\\n",ret,&count,count);
        printf("【parent】the pid=%d\\n",getpid());
    
	return 0;

运行结果

unbuntu14:~/crazymo$ gcc forkdemo.c -o vforkunbuntu14:~/crazymo$ ./vfork
【parent】assign shared var on &count=0x7ffe774fe418 in pid=7957
【parent】fork in pid=7957
【child】start in pid=7958
【child】assign on &count=0x7ffe774fe418  with count=100
 //这里会sleep(2) 然后 父进程才会继续执行
【parent】continue in parent pid=7957
【parent】ret=7958, &count=0x7ffe774fe418 , count=100
【parent】the pid=7957

从以上运行结果中我们可以得到简单的结论:父、子进程共享的count 变量其虚拟内存地址一致,在调用vfork之后父进程会挂起,子进程对count 修改会体现在父进程中

四、fork 简单测试

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

/**
* forkdemo.c
* n success ,the PID of the child process is returned int the parent,and 0 is returned
* in the child .
* onFailure,-1 is returned in the parent,no child process is created.and errno is set appropriately.
*
*/
int main(int argc, char* argv[])

    pid_t ret;
    int count =0;
	//在父进程的空间中,定义一个count 共享变量
	printf("【parent】assign shared var on &count=%p in pid=%d\\n",&count,getpid());
    printf("【parent】fork in pid=%d\\n",getpid());
    //ret=vfork(); //vfork 父子进程共享对象count,父、子进程共享的count 变量其虚拟内存地址一致,在调用vfork之后父进程会挂起,子进程对count 修改会体现在父进程中
	ret=fork();//fork 父、子进程共享变量的地址,父进程不共享变量的值,父、子进程中的count 变量的地址一样,但是对应的值不一样,在父进程中count值为0,在子进程中count值为100,**父、子进程共享的count 变量其虚拟内存地址一致,但调用fork之后父进程不会挂起,子进程对count 修改不一定会体现在父进程中。**

    if(ret==0)
    
        printf("【child】start in pid=%d\\n",getpid());
        count=100;
        printf("【child】assign on &count=%p  with count=%d\\n",&count,count);
        sleep(2);
		_exit(0);//退出子进程,必须调用,因为使用vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数退出,否则会报vfork: cxa_atexit.c:100: __new_exitfn: Assertion `l != ((void *)0)' failed
        //execl("./vfork2",0);
    
    else
    
		printf("【parent】continue in parent pid=%d\\n",getpid());
        printf("【parent】ret=%d, &count=%p , count=%d\\n",ret,&count,count);
        printf("【parent】the pid=%d\\n",getpid());
    

运行结果

unbuntu14:~/crazymo$ gcc forkdemo.c -o fork
unbuntu14:~/crazymo$ ./fork
【parent】assign shared var on &count=0x7fffc709ab18 in pid=7950
【parent】fork in pid=7950
【parent】continue in parent pid=7950
【parent】ret=7951, &count=0x7fffc709ab18 , count=0
【parent】the pid=7950
【child】start in pid=7951
【child】assign on &count=0x7fffc709ab18  with count=100

从以上运行结果中我们可以得到简单的结论:父、子进程共享的count 变量其虚拟内存地址一致,但调用fork之后父进程不会挂起,因此子进程对count 修改不一定会体现在父进程中。

以上是关于Android NDK ——Linux 创建应用进程之 fork vs vfork 小结的主要内容,如果未能解决你的问题,请参考以下文章

Android NDK ——Linux 创建应用进程之 fork vs vfork 小结

Android 逆向Android 进程注入工具开发 ( Visual Studio 开发 Android NDK 应用 | 使用 Makefile 构建 Android 平台 NDK 应用 )(代码

Android Studio NDK基础使用

Android NDK编译如何强制使用libc++.a的静态链接库

Android 逆向Android 进程注入工具开发 ( Visual Studio 开发 Android NDK 应用 | VS 自带的 Android 平台应用创建与配置 )

如何用Android NDK编译FFmpeg