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

2023-10-14 17:50

本文主要是介绍Android NDK ——Linux 创建应用进程之 fork vs vfork 小结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章大纲

  • 引言
  • 一、Unix 进程概述
  • 二、fork、vfork、clone
  • 三、vfork 简单测试
  • 四、fork 简单测试

引言

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 小结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/212152

相关文章

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

Python中re模块结合正则表达式的实际应用案例

《Python中re模块结合正则表达式的实际应用案例》Python中的re模块是用于处理正则表达式的强大工具,正则表达式是一种用来匹配字符串的模式,它可以在文本中搜索和匹配特定的字符串模式,这篇文章主... 目录前言re模块常用函数一、查看文本中是否包含 A 或 B 字符串二、替换多个关键词为统一格式三、提

Java MQTT实战应用

《JavaMQTT实战应用》本文详解MQTT协议,涵盖其发布/订阅机制、低功耗高效特性、三种服务质量等级(QoS0/1/2),以及客户端、代理、主题的核心概念,最后提供Linux部署教程、Sprin... 目录一、MQTT协议二、MQTT优点三、三种服务质量等级四、客户端、代理、主题1. 客户端(Clien

Linux中SSH服务配置的全面指南

《Linux中SSH服务配置的全面指南》作为网络安全工程师,SSH(SecureShell)服务的安全配置是我们日常工作中不可忽视的重要环节,本文将从基础配置到高级安全加固,全面解析SSH服务的各项参... 目录概述基础配置详解端口与监听设置主机密钥配置认证机制强化禁用密码认证禁止root直接登录实现双因素

在Linux终端中统计非二进制文件行数的实现方法

《在Linux终端中统计非二进制文件行数的实现方法》在Linux系统中,有时需要统计非二进制文件(如CSV、TXT文件)的行数,而不希望手动打开文件进行查看,例如,在处理大型日志文件、数据文件时,了解... 目录在linux终端中统计非二进制文件的行数技术背景实现步骤1. 使用wc命令2. 使用grep命令

python如何创建等差数列

《python如何创建等差数列》:本文主要介绍python如何创建等差数列的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录python创建等差数列例题运行代码回车输出结果总结python创建等差数列import numpy as np x=int(in

Linux如何快速检查服务器的硬件配置和性能指标

《Linux如何快速检查服务器的硬件配置和性能指标》在运维和开发工作中,我们经常需要快速检查Linux服务器的硬件配置和性能指标,本文将以CentOS为例,介绍如何通过命令行快速获取这些关键信息,... 目录引言一、查询CPU核心数编程(几C?)1. 使用 nproc(最简单)2. 使用 lscpu(详细信

怎么用idea创建一个SpringBoot项目

《怎么用idea创建一个SpringBoot项目》本文介绍了在IDEA中创建SpringBoot项目的步骤,包括环境准备(JDK1.8+、Maven3.2.5+)、使用SpringInitializr... 目录如何在idea中创建一个SpringBoot项目环境准备1.1打开IDEA,点击New新建一个项