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

相关推荐
枕星而眠4 分钟前
Linux守护进程完全指南:从原理到实战
linux·运维·服务器·c++·后端
网络系统管理5 分钟前
第八届江苏技能状元大赛选拔赛信息通信网络运行管理项目模块D网络服务与系统运维-Linux样题解析
linux·运维·网络
云水一下20 分钟前
Vue.js从零到精通系列(三):组件化基础——Props、Emits、插槽与生命周期
前端·javascript·vue.js
不会C语言的男孩21 分钟前
Linux 系统编程 · 第 2 章:系统调用与库函数
linux·c语言
坤昱24 分钟前
cfs调度类深入解刨——psi科普篇
linux·cfs·psi·cfs调度·eevdf·psi详细分析·linux系统资源监控
SEO_juper1 小时前
新独立站冷启动收录全攻略:配置、推送、抓取配额优化完整手册
前端·谷歌·seo·跨境电商·外贸·geo·独立站
骑上单车去旅行1 小时前
openEuler 22.03 离线源码编译 Zabbix 7.0.27 完整最终整合手册
linux·运维·服务器·zabbix
TinssonTai1 小时前
这个 VS Code 插件让我的 AI Coding 又快又稳 - 旧瓶装新酒
前端·人工智能·程序员
体验家1 小时前
体验家 XMPlus 网页端问卷 SDK 技术解析:用几行 JavaScript 实现精准场景触发与防打扰机制
开发语言·前端·javascript
林九生1 小时前
【实用技巧】MySQL 绿色版一键路径更新脚本详解 —— update_path.bat 深度解析
android·数据库·mysql