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

相关推荐
2501_94452554几秒前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
weixin_4370446427 分钟前
Netbox批量添加设备——堆叠设备
linux·网络·python
hhy_smile28 分钟前
Ubuntu24.04 环境配置自动脚本
linux·ubuntu·自动化·bash
李白你好41 分钟前
Burp Suite插件用于自动检测Web应用程序中的未授权访问漏洞
前端
宴之敖者、1 小时前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei1 小时前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ1 小时前
【Linux 系统】进程间的通信方式
linux·服务器
Abona1 小时前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin1 小时前
Linux 基础IO
java·linux·服务器
The Chosen One9851 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器