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

前言

最近在看Android底层源码的时候发现fdsan这个检测工具,全名为file descriptor sanitizer,是用来检测fd的use-after-close和double-close错误,避免非必要的安全隐患。当然这个工具不是本文的重点,后续需要可以做个相关的介绍,但是在这个知识里面看到了其一些限制,发现在vfork的子进程里是无法正常工作的,原因是vfork得到的子进程虽然会拷贝父进程的fd,但是使用的地址空间仍然属于父进程,这里面会涉及到fdsan的运行原理,我们暂且不表,但是这里出现了vfork这个操作进程的方式,于是三省吾身回顾扫盲了下,fork、vfork、clone、exec等方式创建进程的区别,其中还也不免涉及到COW技术(老生常谈的技术了)。

Linux

Android目前的内核还是基于宏内核Linux开发的,而Linux是Unix的GNU版本。众所周知进程是资源的单位,线程是调度的单位。也就是说每个进程都有自己独立的资源,独立的 task_struct,Linux内核中没有独立的"线程"结构。这里面有历史的原因Unix里面其实只有进程,而线程是 POSIX标准定义的,而Linux为了对齐(适配)通用标准,所以Linux的线程就是轻量级进程,换言之基本控制结构和Linux的进程是同样的(都是经过struct task_struct管理)。

Linux的用户进程不能直接被创建出来,因为不存在这样的API。它只能从某个进程中复制出来,再通过exec这样的API来切换到实际想要运行的程序文件。

Linux复制的API有fork,vfork,clone都是系统调用,这三个函数分别调用了sys_fork、sys_vfork、sys_clone,最终都调用了do_fork函数,差别在于参数的传递和一些基本的准备工作不同,主要用来linux创建新的子进程或线程。

进程

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

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

进程的必备条件:

  1. 有一段程序供其执行,即使和进程共用;
  2. 有进程专用的系统堆栈空间;
  3. 有task_struct数据结构,也就是进程控制块。有了这个数据结构,进程才能成为内核调度的一个基本单位,从而接受内核的调度。但同时,该数据结构也记录着进程所占用的各项资源,内核为每个进程分配一个task_struct结构时,实际上是分配两个连续的物理页面(共8192字节)。

​ 4.有独立的存储空间,即用户空间堆栈。

这四条都是必要条件,缺一不可,否则就只能称作是线程了。如果完全没有用户空间,就称作是"内核线程",而共享用户空间就称作是"用户线程",也统称线程。

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

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

fork

arduino 复制代码
#include <unistd.h> pid_t fork(void);

fork()函数调用成功:返回两个值; 父进程:返回子进程的PID;子进程:返回0;失败:返回-1;fork函数只被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID。同时,父进程fork后为子进程生成了一个PCB。

fork 后子进程得到返回值0的原因是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID(进程ID 0总是由内核交换进程使用,所以 一个子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。但很多情况下fork后紧跟着exec执行新的程序段,因此不需要完全复制,出于效率考虑,linux中引入了COW技术,其需要依赖硬件中的MMU (memoy ,management uni)。

COW

其核心思想是父进程和子进程共享页帧而不是复制页帧。因为只要页帧被共享,它们就不能被修改,即页帧被保护。因此无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写,这样原来的页帧仍然是写保护的,即当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。

运行时分阶段看,在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。

盗个图,特此感谢大佬辛勤结晶:

arduino 复制代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
void main()
{
char str[6]="hello";
pid_t pid=fork();
if(pid==0)
  {
        str[0]='b';
printf("子进程中str=%s\n",str);
printf("子进程中str指向的首地址:%x\n",(unsigned int)str);
    }
else
  {
        sleep(1);
printf("父进程中str=%s\n",str);
printf("父进程中str指向的首地址:%x\n",(unsigned int)str);
    }
}

结果:

ini 复制代码
子进程中str=bello
子进程中str指向的首地址:bfdbfc06
父进程中str=hello
父进程中str指向的首地址:bfdbfc06

Android中也存在很多fork的调用,比如init进程创建几个重要的进程,比如Zygote,Zygote孵化其他进程,system_server等。

总结

本文旨在帮各位梳理Linux不同创建进程的方式,因一个小知识点为引子深入研究下各种方式的原理,怕文章篇幅过多,占用大家过多精力,于是拆成上下两篇文章,下篇敬请期待,欢迎关注。 微信公众号首发,感谢各位老铁一键三连,欢迎留言交流。

相关推荐
喵叔哟7 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django