操作系统系列:Unix进程系统调用fork,wait,exec

news2024/11/17 11:37:20

操作系统系列:Unix进程系统调用

  • fork系统调用
    • fork()运用的小练习
  • wait系统调用
  • Zombies
  • exec 系列系统调用

开发者可以查看创建新进程的系统调用,这个模块会讨论与进程相关的Unix系统调用,下一个模块会讨论Win32 APIs相关的进程。

fork系统调用

在经典的Unix系统,所有的进程都是用fork()创建的,这个系统调用创建一个新的进程,这个新的进程是调用fork的进程的完美副本,这个调用进程被称为父进程,而fork创建的新进程被称为子进程。父子进程都是可运行的,并且在fork系统调用后立即开始运行。
这是函数原型:

#include <sys/tepes.h>
#include <unistd.h>
pid_t fork(void);

数据类型pid_t是进程id的类型,在所有系统上它都是一个无符号整型。fork()的返回值很重要,在父进程中,这个从fork()中返回的值是子进程的进程id。在子进程中,fork()的返回值是0。在有错误的情况下,fork()会返回一个负值。请看示例:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
extern int errno;
int main()
{
	pid_t pid;
	pid = fork();
	if(pid == 0)
		printf("I'm the child\n");
	else if(pid > 0){
		printf("I'm the parent\n");
		printf("child pid is %d\n",pid);
	}
	else{/* pid < 0 */
		perror("Error forking");
		fprintf(stderr,"errno is %d\n",errno);
	}
	return 0;
}

当正在运行的程序执行第8行的fork系统调用时,就会创建一个新进程,该子进程与父进程具有完全相同的代码。这个例子中没有其他变量,但是如果父进程中碰巧有一个名为 x 的变量,其值为 17,那么子进程中也会有一个名为 x 的变量,其值为 17。 父子进程都会在fork这行以后开始运行,开发者区分父子进程的唯一方式,是通过fork的返回值。
下面这个图展示了在进程1234调用fork创建子进程1235之前和之后的进程图。
进程图
在正常情况下,调用 fork 不太可能失败。 但是,所有 Unix 系统对单个用户可以运行的进程总数以及进程表中同时存在的进程总数都有限制,因此如果创建新进程会导致超过两者中的任意一个限制,就会失败并返回负值,也不会创建子进程。
以下几条父子进程是一样的:

  • 文本段(代码段)
  • 所有变量的值(除了fork()的返回值)
  • 环境
  • 进程优先级
  • 控制终端
  • 当前工作目录
  • 打开文件描述符

要知道,尽管这些变量的值是一样的,所有不同的数据段,包括运行时的堆栈都已被拷贝,所以每个变量有两个实例,从而允许每个进程各自独立地更新这些数据。
父进程和子进程的区别如下:

  • 进程号
  • 父进程id
  • 有关资源分配的数据。 例如,子进程中的总运行时间设置为零,子进程的进程启动时间设置为当前时间

要知道每个进程除了init进程(init进程pid为0,并且是在启动时创建的第一个进程,在系统shut down之前一直运行)都有一个父进程,因此存在一个以 init 为根的进程树。

这是 fork 的作用:

  • 给子进程的数据和栈预留交换空间
  • 分配新的pid和内核进程结构
  • 初始化内核进程结构,某些字段(即用户id,组id,信号掩码)是从父进程拷贝的,某些字段设置为0(即cpu使用率),其它字段像ppid点是子进程的特定值
  • 为子进程分配地址转换映射
  • 增加子进程到进程集,共享父进程正在执行的文本区域
  • 复制父进程的数据和堆栈区域
  • 获取对子进程继承的共享资源的引用,比如打开的文件
  • 通过拷贝父进程寄存器初始化硬件上下文
  • 让子进程运行起来,并且将其放入调度进程队列
  • 安排子进程以零值从fork返回
  • 返回子进程的id给父进程

fork()运用的小练习

这是一个简短的程序。 假设本机上没有其他进程在运行,那么当创建一个新进程时,它的进程id比当前进程大1。
系统调用getpid()返回了调用它的进程id,系统调用getppid()返回父进程的进程id。
Note: 系统调用getpid和getppid不能失败。
该进程的父进程是 shell。 如果这个程序被编译并运行,并且在一次特定的运行过程中,它的进程id是1000,而shell的进程id是500,那么这个程序会打印什么?

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
	pid_t p,x,y;
	x = getpid();
	printf("%d\n",x);	/* prints 1000 */
	y = getppid();
	printf("%d\n",y); /* prints 500 */
	p = fork();
	if(p > 0) {
		sleep(1); /* sleep for on second */
		printf("%d\n",p);
		x = getpid();
		printf("%d\n",x);
		y = getppid();
		printf("%d\n",y);
		exit(0);
	}
	else if(p == 0){
		printf("%d\n",p);
		x = getpid();
		printf("%d\n",x);
		y = getppid();
		printf("%d\n",y);
		exit(0);
	}
	return 0;
}

wait系统调用

父进程和子进程谁先运行是不确定的,这个术语叫竟态条件。因此这个程序有2个可能的输出:

I'm the child
I'm the parent, child pid is 22970

是其中一种,另一种是:

I'm the parent, child pid is 22970
I'm the child

父进程可以通过wait()系统调用来控制这一点,这个调用会导致在子进程消亡之前,父进程都是被阻塞的。如果父进程没有子进程,那么wait会立即返回。
这里是函数原型:

 #include <sys/types.h>
 #include <sys/wait.h>

 pid_t wait(int *stat_loc);

wait 的返回值是死亡子进程的进程 ID,这个简短的程序用来演示这一点。

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
extern int errno;
int main()
{
	pid_t pid,retval;
	int status;
	pid  = fork();
	if(pid == 0)
			printf("I'm the child\n");
	else if(pid > 0){
			retval = wait(&status);
            printf("I'm the parent,");
            printf("the child %d has died\n",retval);
	}
	else{ /* pid < 0 */
			perror("Error forking");
			  fprintf(stderr,"errno is %d\n",errno);
	}
	return 0;
}

如果父进程碰巧先运行,它将执行 wait 系统调用,这会导致进程阻塞,它保持阻塞状态,直到子进程终止,此时一个信号被发送到父进程,唤醒它并返回到可运行状态。 如果子进程恰好先运行并在父进程运行之前终止,则对 wait 的调用将立即返回。 无论哪种情况,返回值都是子进程的进程 ID。

垂死孩子的父母可能想知道孩子是如何死亡的,所以子进程可能想向父进程发送消息,这两者都是使用传递给 wait 的参数来完成的。 因为这是一个引用参数,所以它的值是由系统调用设置的。 最低有效字节指示子进程如何死亡。 如果子进程正常终止(即进程到达 main() 的末尾或调用了 exit() 系统调用),则状态的最低字节将为零。 如果子进程异常终止(例如,由于内存异常错误(分段错误)或用户发送终止信号(cntl-c)而终止),则最低字节将设置为终止它的信号的数值 。

如果子进程通过调用 exit() 正常终止,则子进程可以将参数传递给 exit(),并且该值将位于status的第二个字节中。 例如,如果子进程调用 exit(5),则状态的二进制值将是:

00000000 00000000 00000101 00000000
十六进制表示为 00 00 05 00

科普一下C语言的运算符:

这里, >>是右移运算符,<< 是左移运算符,& 是按位与运算符,| 是按位或运算符。
开发者可以使用它们来检查状态的每个字节的值。 例如,要检查最低位字节是否为零,请使用按位与运算符和 0xFF(C 中数字常量前面的 0x 表示该值是十六进制的)。

  if (status & 0xFF != 0)  
     printf("The child died abnormally");

要检查第三个字节的值,请将值右移 8 位,然后使用 0xFF 执行逻辑与。

   int temp;
   ....
   temp = status >> 8; /* right shift */
   temp = temp & 0xFF; 
   printf("exit status was %d\n",temp);

如果一个进程在其所有子进程终止之前终止,则子进程将成为“孤儿”。 由于除 init 之外的所有进程都有父进程,因此“孤儿”进程会被 init 进程回收。

Zombies

  • 如果子进程在其父进程调用 wait() 之前死亡,则父进程可能会在稍后的某个时间调用 wait,并且需要有关已死亡子进程的状态的信息。 在这种情况下,进程并没有真正终止,只是保留了一些信息。
  • 已终止但其父进程未调用 wait() 的进程称为僵尸进程。 僵尸进程虽然不消耗其他资源,但在操作系统的进程表中占据一个槽位。
  • 当您使用 ps 命令检查计算机上的进程时,僵尸的状态已失效。
  • 当父级调用 wait 并获取有关该子级的信息或父级死亡时,僵尸将被终止,因为父级为 init 的僵尸将被杀死。

exec 系列系统调用

  • 由前文可知,由于 fork 只能创建其自身的副本,因此它的用途有限。
  • fork 调用通常与另一个系统调用 exec 一起使用,后者用全新的进程映像覆盖整个进程空间,新映像会从头开始执行。
  • Exec 实际上是有六个系统调用的族,其中最简单的是 execl 下面是函数原型:
     #include <unistd.h>  /* standard unix header file */

     int execl(const char *path, const  char  *arg0,  ...,  const
     char *argn, NULL);

第一个参数 path 应该是可执行程序的路径名。 其余参数是要作为 argv 传递给该程序的参数。 参数列表以 NULL 结束。

这是一个简短的示例程序。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <wait.h>
extern int errno;
int main()
{
    pid_t p;
    p=fork();
    if (p == 0)  { /* child */
         execl("/bin/ls", "ls", "-l", NULL);
         perror("Exec failed");
    }
    else if (p > 0) {
        wait(NULL);
        printf("Child is done\n");
    }
    else {
        perror("Could not fork");
    }
    return 0;
}

该程序会创建一个新进程。 子进程的映像被命令 /bin/ls 的映像覆盖,并且使用两个参数 ls 和 -l 来调用它(回想一下,按照约定,argv[0] 是命令的名称),然后子进程运行 ls,当它终止时,父进程被唤醒,显示其消息,并且也终止。
内存中的进程布局
任何 exec 调用都可能失败,比较明显的失败原因是路径不是可执行文件的路径名。

  • 如果任何 exec 调用成功,则不会返回,因为调用进程的所有代码都会被新映像覆盖。
  • 如果失败,它会像任何其他系统调用一样返回负值,但不需要检查这一点,因为它只有在失败时才能返回。
  • 这就是为什么 perror 调用之前没有 if 的原因,程序到达该行的唯一方法是调用 execl 失败。

exec 系列中还有其他五个系统调用,都是用新的镜像覆盖当前进程,而他们的不同之处仅在于他们所接受的参数以及其他一些细微的方面。

int execv(const char *path, char *const argv[])
此调用与 execl 相同,只是它只接受两个参数,第二个参数是参数向量。
示例程序:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <wait.h>
extern int errno;
int main()
{
    pid_t p;
    char *args[100];

    args[0]="ls";
    args[1]="-l";
    args[2]=NULL;
    p=fork();
    if (p == 0)  { /* child */
         execv("/bin/ls", args);
         perror("Exec failed");
    }
    else if (p > 0) {
        wait(NULL);
        printf("Child is done\n");
    }
    else {
        perror("Could not fork");
    }
    return 0;
}

int execle(const char *path, const char *arg0, …, const char *argn, char * /NULL/, char *const envp[])
与 execl 一样,此调用采用可变数量的参数,但其最终参数是表示新环境的向量。
默认情况下,执行的进程的环境与父进程的环境相同,但这允许用户更改环境。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <wait.h>
extern int errno;
int main()
{
    pid_t p;
    char *envp[100];
    envp[0]="USER=ingallsr";
    envp[1]="HOME=/cs/ingallsr";
    envp[2]="PWD=/cs/ingallsr/public.html/OS/c4";
    envp[3]=NULL;    

    p=fork();
    if (p == 0)  { /* child */
      execle("/bin/ls", "ls", "-l", NULL, envp);
         perror("Exec failed");
    }
    else if (p > 0) {
        wait(NULL);
        printf("Child is done\n");
    }
    else {
        perror("Could not fork");
    }
    return 0;
}

int execve(const char *path, char *const argv[], char *const envp[])
与 execv 相同,只是它传递环境向量作为第三个参数。

int execlp(const char *file, const char *arg0, …, const char *argn, char * /NULL/)
这与上面的调用不同,它的第一个参数只是文件名而不是路径,并且调用在 PATH 环境变量中搜索可执行文件。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <wait.h>
extern int errno;
int main()
{
    pid_t p;
    p=fork();
    if (p == 0)  { /* child */
         execlp("ls", "ls", "-l", NULL);
         perror("Exec failed");
    }
    else if (p > 0) {
        wait(NULL);
        printf("Child is done\n");
    }
    else {
        perror("Could not fork");
    }
    return 0;
}

int execvp(const char *file, char *const argv[])
与 execlp 相同,只是参数作为单个参数传递。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1323015.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智能优化算法应用:基于学生心理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于学生心理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于学生心理学算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.学生心理学算法4.实验参数设定5.算法…

VS+Qt 打包Python程序

书接上回&#xff0c;调用C调用python&#xff0c;下面来谈谈随exe文件打包。 先说下环境vs2019Qt5.12.11python3.8&#xff0c;这里需要注意如果你要适配Win7的系统&#xff0c;python最好是9以下&#xff0c;以上不兼容&#xff0c;也没时间找方法&#xff0c;找到评论说下 如…

实现个人日志命令行工具(C语言)

〇、前言 中午上课的时候&#xff0c;打开 github 看了一下个人主页&#xff0c;虽然最近很忙&#xff0c;但是这个活动记录有点过于冷清&#xff1a; 于是我就想着写一个日志命令行工具&#xff0c;输入以下命令就能将我的日志立即同步到 github 上&#xff1a; mylog toda…

Jenkins+Docker+Gitee搭建自动化部署平台

目录 服务器准备 Docker安装 yum 包更新到最新 设置yum源 安装docker 启动和开机启动 验证安装是否成功 Jenkins安装 拉取镜像 创建映射目录 运行镜像 运行出错 修正权限 重新运行镜像 新建安全组&#xff0c;放通8080端口 激活Jenkins Jenkins插件 Jenkins全…

el-form与el-upload结合上传带附件的表单数据(前端篇)

1.写在之前 本文前端采用Vue element-plus技术栈&#xff0c;前端项目参考yudao-ui-admin-vue3项目与Geeker-Admin项目。 这篇文章是el-form与el-upload结合上传带附件的表单数据&#xff08;后端篇&#xff09;-CSDN博客姐妹篇&#xff0c;后端篇文章主要讲的是后端的实现逻…

限流原理与实践:固定窗口、滑动窗口、漏桶与令牌桶解析

方案一、固定窗口限流算法 这里我们通过一个 demo 来介绍固定窗口限流算法。 创建一个 FixWindowRateLimiterService 类。 Service public class FixWindowRateLimiterService {Resourceprivate StringRedisTemplate stringRedisTemplate;private static final DefaultRedisSc…

新手上路:自动驾驶行业快速上手指南

文章目录 1.自动驾驶技术的发展1.1 工业革命驱动自动驾驶技术发展1.2 想象中的未来&#xff1a;科幻作品中的自动驾驶汽车1.3 自动驾驶技术萌芽与尝试1.4 百花争鸣&#xff1a;自动驾驶科技巨头与创业公司并进 2.个人开发者&#xff0c;如何玩转自动驾驶&#xff1f;2.1 灵活易…

Opencv实验合集——实验六:模板匹配

1.概念 模板匹配旨在在图像中找到与给定模板最相似的部分。其核心思想是通过滑动模板&#xff0c;计算每个位置与模板的相似性&#xff0c;然后找到最匹配的位置。这一过程常涉及选择匹配度量方法&#xff0c;如平方差匹配、归一化平方差匹配、相关性匹配等。模板匹配在目标检…

git缓存区、本地仓库、远程仓库的同步问题(初始化库无法pull和push)

git新建库与本地库同步 gitee使用教程&#xff0c;git的下载与安装接不在叙述了。 新建远程仓库 新建远程仓库必须要使用仓库提供的api&#xff0c;也就是仓库门户网站&#xff0c;例如gitee&#xff0c;github&#xff0c;gitlab等。在上图中使用gitee网址中新建了一个test仓…

【无语】Microsoft Edge 浏览器不显示后台返回的数值数据

Microsoft Edge 禁用 JSON 视图 写在前面禁用 JSON 视图 写在前面 遇到一个有意思的事情&#xff0c;在用 Microsoft Edge 浏览器发送请求测试时发现&#xff0c;后端返回的数值数据没有正常展示&#xff0c;而是类似查看源码的结果&#xff0c;只显示了一个行号1&#xff0c;…

SpringMVC01

SpringMVC 1. 学习⽬标2. 什么叫MVC&#xff1f;3. SpringMVC 框架概念与特点4. SpringMVC 请求流程5. Spring MVC 环境搭建6. URL 地址映射配置7. 参数绑定8. JSON 数据开发JSON普通数组步骤1:pom.xml添加依赖步骤2&#xff1a; 修改配置⽂件步骤3. 注解使⽤ 1. 学习⽬标 2. 什…

Android Studio: 解决Gradle sync failed 错误

文章目录 1. 前言2. 错误情况3. 解决办法3.1 获取gradle下载地址3.2 获取gradle存放目录3.3 替换并删除临时文件3.4 触发Try Again 4. 执行成功 1. 前言 今天调试项目&#xff0c;发现新装的AS&#xff0c;在下载gradle的过程中&#xff0c;一直显示连接失败&#xff0c;Gradl…

Zookeeper-快速开始

Zookeeper介绍 简介&#xff1a;ZooKeeper 是一个开源的分布式协调框架&#xff0c;是Apache Hadoop 的一个子项目&#xff0c;主要用来解决分布式集群中应用系统的一致性问题。 设计目标&#xff1a;将那些复杂且容易出错的分布式一致性服务封装起来&#xff0c;构成一个高效…

数据可视化---饼图、环形图、雷达图

类别内容导航机器学习机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归机器学习算法—聚类机器学习算法—异常检测机器学习算法—时间序列数据可视化数据可视化—折线图数据可视化—箱线图数据可视化—柱状图数据可视化—饼图、环形图、雷达图统计学检验箱…

linux:掌握systemctl命令控制软件的启动和关闭、掌握使用ln命令创建软连接

掌握使用systemctl命令控制软件的启动和关闭 一&#xff1a;systemctl命令&#xff1a; Linux系统很多软件(内置或第三方)均支持使用systemctl命令控制:启动停止、开机自启 能够被systemctl管理的软件一般也称之为:服务 语法: systemctl | start | stop | status | enable …

JDK各个版本特性讲解-JDK17特性

JDK各个版本特性讲解-JDK17特性 一、JAVA17概述二、语法层面的变化1.JEP 409&#xff1a;密封类2.JEP 406&#xff1a;switch模式匹配&#xff08;预览&#xff09; 三、API层面变化1.JEP 414&#xff1a;Vector API&#xff08;第二个孵化器&#xff09;2.JEP 415&#xff1a;…

10000字讲解TCP协议(确认应答,超时重传,三次握手,四次挥手等等众多机制)以及UDP协议(UDP报文,校验和)

文章目录 UDP协议&#xff1f;什么是校验和&#xff1f;基于UDP的应用层协议(了解) TCP协议确认应答(可靠性机制)超时重传(可靠性机制)连接管理(可靠性机制)三次握手(重点)四次挥手(重点) 三次握手和四次挥手时客户端和服务器的状态滑动窗口(效率机制)流量控制(效率机制)窗口探…

【论文笔记】动态蛇卷积(Dynamic Snake Convolution)

精确分割拓扑管状结构例如血管和道路&#xff0c;对医疗各个领域至关重要&#xff0c;可确保下游任务的准确性和效率。然而许多因素使分割任务变得复杂&#xff0c;包括细小脆弱的局部结构和复杂多变的全局形态。针对这个问题&#xff0c;作者提出了动态蛇卷积&#xff0c;该结…

腾讯云消息队列11月产品月报 | RocketMQ 5.x 国际站上线

2023年 11月动态 消息队列 RocketMQ 版 1、5.x 形态国际站上线 国际站上线 5.x 集群全系列&#xff0c;第一批先开放新加坡和硅谷地域。 控制台链接&#xff1a;https://console.tencentcloud.com/trocketmq 2、 无感迁移能力 支持用户白屏化操作&#xff0c;将自建的 Roc…

搭建 ElasticSearch 集群环境

安装基础环境 我们用虚拟机创建出3台机器&#xff0c;查看centos版本为7.9 [roots1 ~]# cat /etc/centos-release CentOS Linux release 7.9.2009 (AltArch)下载相关命令 yum -y install vim* yum -y install net-tools yum -y install lsof yum -y install wget yum -y ins…