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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链

Linux磁盘分区、格式化和挂载方式

《Linux磁盘分区、格式化和挂载方式》本文详细介绍了Linux系统中磁盘分区、格式化和挂载的基本操作步骤和命令,包括MBR和GPT分区表的区别、fdisk和gdisk命令的使用、常见的文件系统格式以... 目录一、磁盘分区表分类二、fdisk命令创建分区1、交互式的命令2、分区主分区3、创建扩展分区,然后

Linux中chmod权限设置方式

《Linux中chmod权限设置方式》本文介绍了Linux系统中文件和目录权限的设置方法,包括chmod、chown和chgrp命令的使用,以及权限模式和符号模式的详细说明,通过这些命令,用户可以灵活... 目录设置基本权限命令:chmod1、权限介绍2、chmod命令常见用法和示例3、文件权限详解4、ch

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

什么是cron? Linux系统下Cron定时任务使用指南

《什么是cron?Linux系统下Cron定时任务使用指南》在日常的Linux系统管理和维护中,定时执行任务是非常常见的需求,你可能需要每天执行备份任务、清理系统日志或运行特定的脚本,而不想每天... 在管理 linux 服务器的过程中,总有一些任务需要我们定期或重复执行。就比如备份任务,通常会选在服务器资