详解进程控制

news2024/11/26 12:18:28

目录

一、进程创建

fork()

写时拷贝

fork的应用场景

二、进程退出

什么是进程退出码?

退出码的含义

进程退出方法

三、进程等待

进程等待的必要性

进程等待的方法

wait

waitpid

status

阻塞与非阻塞

四、进程替换

替换原理

替换函数

命名理解

简易shell编写


一、进程创建

在Linux系统下我们可以使用fork()函数为当前进程创建一个子进程。

fork()

#include<unistd.h>

pid_t   fork(void);

返回值:子进程中返回0,父进程返回子进程pid,出错返回-1

在fork()系统调用中,当一个进程调用fork后,控制转移到内核中的fork代码,内核会执行以下操作:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中 fork返回
  • 开始调度器调度

操作系统会为该进程创建一个几乎一模一样的子进程。当fork完成时,两个进程的内存、寄存器、程序计数器等状态都完全一致,他们的代码共享。但它们是完全独立的两个进程,拥有不同的PID和虚拟内存空间,在fork完成后它们会各种独立地执行,互不干扰。即进程具有独立性。

如果大家对进程之间如何保持独立性感兴趣可以读一下我之前所写的博客 进程地址空间

演示:

 运行结果:

我们可以看到子进程对a的修改并未影响到父进程,子进程与父进程相互独立,而从getpid()与getppid()我们可以看到他们确实互为父子关系。需要注意的一点是fork之后,父进程和子进程谁先执行完全由调度器决定。

写时拷贝

当使用fork()系统调用创建一个新进程时,子进程会继承父进程的地址空间、数据、环境变量等资源。这些资源并不是立即复制给子进程的,而是让父子进程共享这些资源。只有当其中一个进程试图修改这些共享资源时,操作系统才会进行实际的复制操作,即写时拷贝。

Q:为什么要写时拷贝

A:写时拷贝可以减少不必要的内存使用,因为多个进程或线程可能只是读取共享数据,而不会对其进行修改。在这些情况下,共享同一个数据副本是有效率的。

需要注意的是,代码也会进行写时拷贝,这点我们将会在进程替换为大家介绍。

fork的应用场景

多进程并发处理任务:

在需要同时处理多个任务的情况下,可以使用fork函数创建多个子进程来并发处理这些任务。每个子进程独立运行,可以同时执行不同的任务,从而加快任务处理速度。父进程可以通过等待子进程结束并获取子进程的返回结果,从而实现多任务的并发处理。

服务器编程:

在服务器编程中,使用fork函数可以实现并发处理客户端的请求。当有新的连接请求到达服务器时,可以使用fork函数创建一个子进程来处理该连接,而父进程继续监听新的连接。这样可以同时处理多个客户端请求,提高服务器的并发性能。

创建守护进程:

fork函数也可以用于创建守护进程。守护进程是在后台运行的特殊进程,通常用于执行系统级任务。通过fork函数创建一个子进程,并在子进程中执行需要的任务,然后让父进程退出,子进程就会成为新的会话组长,从而成为一个守护进程。

shell程序:

在shell程序中,fork函数被用于创建子进程来执行用户输入的命令。当用户输入一个命令时,shell程序会使用fork函数创建一个子进程,然后在子进程中执行该命令。这样可以让shell程序在等待用户输入新的命令时,子进程可以继续执行上一个命令。

在本文我们会用所学知识实现一个简易的shell。

、进程退出

进程退出有且仅有三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

这三种情况可分为异常退出和正常退出,前两种情况为正常退出,第三种情况为异常终止。

当程序退出时会返回一个退出码(我们可以通过 echo $? 查看进程退出码)

什么是进程退出码?

要回答这个问题,我们首先要重新建立对main函数的认知。

 我们知道一个C/C++函数必须要有main函数,main函数是C/C++ 程序的标准入口。操作系统通过·调用main函数来执行程序,但main函数并不是被操作系统直接调用的。

当程序开始执行时,它首先会执行一些初始化工作,如设置全局变量、分配堆和栈空间等,然后调用 main 函数。尽管 main函数是代码的入口,但它并不是程序启动的第一个函数。在 main函数之前,还有其他的启动代码(如 __tmainCRTStartup)被执行。

启动代码是由 C 运行时库提供的,它的职责是设置程序运行所需的环境,包括初始化运行时堆、栈、全局变量等,然后调用 main函数。这个启动代码最终是由操作系统的加载器调用的,加载器负责将程序加载到内存中,并设置适当的上下文以便程序可以执行。

当 main函数执行完毕后,它会返回一个整数值,这个值被称为退出码(exit code)或返回码。这个退出码实际上是传递给操作系统的,表示程序是正常结束(通常返回0)还是出现了某种错误(返回非0值)。操作系统可以使用这个退出码来判断程序的执行状态,并在需要时进行相应的处理。

现在我们明白了退出码的本质是main函数的返回值。

退出码的含义

每一个退出码都对应一种错误,我们可以通过strerror函数获取对应的错误信息。

执行结果:

我们可以看到退出码都有对应的字符串含义,帮助用户确认执行失败的原因,

但实际上这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

进程退出方法

正常退出:这通常发生在程序成功执行并完成了其所有任务后。在C/C++中,当main函数执行完毕并返回时,进程会正常退出。main函数的返回值就是进程的退出码,通常返回0表示成功,非0值表示某种错误或异常情况。

异常终止:这是由于程序遇到了无法处理的错误或异常情况,如运行时错误、内存溢出、除零错误等。在这种情况下,进程会突然终止,并且通常会返回一个非零的退出码,以指示程序没有成功完成。

通过系统调用退出:程序员可以使用特定的系统调用来明确结束进程。

#include<unistd.h> 

void _exit(int status);

#include<unistd.h>

void exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

在C/C++中,可以使用exit()函数来结束程序。当调用exit()时,程序会立即终止,并且传递给exit()的参数将作为进程的退出码。_exit()和也是可以用来结束进程的函数,但它的行为与exit()略有不同。例如,_exit()会立即结束进程,而不会执行任何后续的清理操作,如关闭文件描述符或执行已注册的终止处理程序。

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

1. 执行用户通过 atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

实例:

可以看到_exit()并不会刷新缓冲区。

三、进程等待

进程等待的必要性
  • 防止僵尸进程:当子进程结束后,如果父进程没有对其进行处理(即没有读取子进程的退出状态信息),那么这个子进程就会成为僵尸进程。僵尸进程会占用系统资源,并且不能被操作系统正常清理,从而导致内存泄漏等问题。通过进程等待,父进程可以及时回收子进程的资源,避免僵尸进程的产生。这个问题我们在之前的博客Linux进程状态讨论过。
  • 获取子进程执行结果:父进程通常需要知道子进程的执行结果,以便根据执行结果进行后续的操作。例如,父进程可能需要检查子进程是否成功完成了任务,或者获取子进程处理的数据结果。通过进程等待,父进程可以获取子进程的退出状态信息,从而了解子进程的执行结果。
  • 同步进程执行顺序:在某些情况下,父进程需要等待子进程完成某些任务后才能继续执行。例如,父进程可能需要等待子进程生成某个文件或数据,然后再对这些文件或数据进行处理。通过进程等待,父进程可以控制自己的执行顺序,确保在继续执行前子进程已经完成了必要的任务。
进程等待的方法
wait

#include<sys/types.h> 

#include<sys/wait.h>

pid_t wait(int*status);

返回值: 成功返回被等待进程pid,失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

wait是一个系统调用,用于使父进程等待一个子进程的结束。它会阻塞父进程,直到有一个子进程结束为止。一旦有子进程结束,wait()会返回该子进程的进程ID,同时父进程可以获取子进程的退出状态。

waitpid

pid_ t waitpid(pid_t pid, int *status, int options);

返回值

        当正常返回的时候waitpid返回收集到的子进程的进程ID;

        如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

        如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数

pid:

        Pid=-1,等待任一个子进程。与wait等效。

        Pid>0.等待其进程ID与pid相等的子进程。

status:

        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status):

        若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

options:

        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。

waitpid(),提供了更多的控制选项。除了具备 wait() 的功能外,它还允许父进程指定等待哪个(些)子进程,以及如何等待。

注意:

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回
status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。 如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。 status不能简单的当作整形来看待,操作系统将它当做一个位图,具体细节如下图(只研究status低16比特位)

我们可以使用位操作,或者宏来获取status中储存的信息 

WIFEXITED(status):

功能:检查子进程是否是正常退出(即调用了 exit() 函数或从 main() 返回)。

返回值:如果是正常退出,则返回非零值(通常为1),否则返回0。

用途:这可以帮助父进程区分子进程是正常结束还是因为其他原因(如信号)终止。

WEXITSTATUS(status):

功能:如果 WIFEXITED(status) 返回非零值,表明子进程是正常退出的,此宏可以提取子进程的退出状态码。

返回值:返回子进程的退出状态码,范围通常是0到255。

用途:可以让父进程知道子进程是如何结束的,根据退出码执行不同的后续操作。

WIFSIGNALED(status):

功能:检查子进程是否是因为接收到信号而终止的。

返回值:如果是信号导致的终止,则返回非零值(通常为1),否则返回0。

用途:帮助父进程识别子进程是否是异常结束,并可能需要采取相应的错误处理措施。

WTERMSIG(status):

功能:如果 WIFSIGNALED(status) 返回非零值,表明子进程是被信号终止的,此宏可以获取导致终止的信号编号。

返回值:返回导致子进程终止的信号编号。

用途:父进程可以根据不同的信号采取相应的处理逻辑,比如重新启动子进程、记录错误日志等。

阻塞与非阻塞

waitpid中的options可以控制父进程等待的方式,当options为0时为阻塞式等待,当options为WNOHANG时为非阻塞式等待

阻塞式等待

当一个进程或线程执行阻塞等待操作时,它会暂停当前的执行,直到等待的条件满足(如子进程终止、I/O操作完成或其他特定事件发生)。在此期间,该进程或线程不会消耗CPU时间,而是被操作系统挂起,直到等待的事件发生。

特点:简单易用,不需要编写额外的逻辑来检查事件状态。但如果等待时间过长,可能会导致资源浪费,尤其是对于需要高响应性的应用程序。

非阻塞等待:

非阻塞等待允许进程或线程在等待某个事件的同时,继续执行其他任务。这意味着调用不会立即阻塞调用者,如果所等待的事件尚未发生,函数会立即返回一个指示状态(如错误代码或特殊值),而不是等待。

特点:提高了程序的响应性和并发性,因为调用者不必等待就可以进行其他工作。但是,这也意味着需要额外的逻辑来处理未完成的事件,如轮询、事件通知或使用异步回调。

四、进程替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换函数

有六种以exec开头的函数,统称exec函数:

#include<unistd.h> 

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ...,char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve.

这些函数之间的关系如下图所示。

简易shell编写

要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

#define MAX_CMD 1024
#define MAX_ARGS 32
char command[MAX_CMD];

// 内置命令处理
int handle_builtin_commands(char *cmd) {
    if (strcmp(cmd, "exit") == 0) {
        printf("退出shell。\n");
        return 1; // 表示已处理内置命令
    }
    return 0; // 未处理,需要外部执行
}

// 解析命令
char **do_parse(char *buff) {
    static char *argv[MAX_ARGS + 1];
    int argc = 0;
    char *ptr = buff;

    while(*ptr != '\0' && argc < MAX_ARGS) {
        while(isspace(*ptr)) ptr++; // 跳过前导空白
        if (*ptr == '\0') break;
        argv[argc++] = ptr;
        while(*ptr != '\0' && !isspace(*ptr)) ptr++; // 找到参数结束
        *ptr++ = '\0'; // 结束当前参数
    }
    argv[argc] = NULL; // 结束标志
    return argv;
}

// 执行命令
int do_exec(char *buff) {
    char **argv = do_parse(buff);
    if (argv[0] == NULL) return -1;

    if (handle_builtin_commands(argv[0])) {
        return 0; // 内置命令已处理,直接返回
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork失败");
        return -1;
    } else if (pid == 0) { // 子进程
        execvp(argv[0], argv);
        perror("执行命令失败");
        exit(EXIT_FAILURE); // 如果execvp失败,则子进程退出
    } else { // 父进程
        waitpid(pid, NULL, 0); // 等待子进程结束
    }
    return 0;
}

// 展示命令提示符并读取用户输入
int do_face() {
    memset(command, 0x00, MAX_CMD);
    printf("minishell$ ");
    fflush(stdout);
    if(scanf("%[^\n]%*c", command) == EOF) {
        // EOF检测,用户可能使用Ctrl+D退出
        printf("\n");
        return 1; // 表示用户结束输入
    }
    return 0;
}

int main() {
    while(1) {
        if (do_face()) {
            printf("再见!\n");
            break; // 用户结束输入,退出循环
        }
        if (do_exec(command) < 0) {
            printf("命令执行失败,请重试。\n");
        }
    }
    return 0;
}

 效果展示:

 

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

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

相关文章

大数据005-hadoop003-了解MR及Java的简单实现

了解MapReduce MapReduce过程分为两个阶段&#xff1a;map阶段、reduce阶段。每个阶段搜键-值对作为输入和输出。 要执行一个MR任务&#xff0c;需要完成map、reduce函数的代码开发。 Hellow World 【Hadoop权威指南】中的以分析气象数据为例&#xff0c;找到每年的最高气温。…

【论文笔记】Language Models are Few-Shot Learners B部分

Language Models are Few-Shot Learners B 部分 回顾一下第一代 GPT-1 &#xff1a; 设计思路是 “海量无标记文本进行无监督预训练少量有标签文本有监督微调” 范式&#xff1b;模型架构是基于 Transformer 的叠加解码器&#xff08;掩码自注意力机制、残差、Layernorm&#…

Spark核心名词解释与编程

Spark核心概念 名词解释 1)ClusterManager&#xff1a;在Standalone(上述安装的模式&#xff0c;也就是依托于spark集群本身)模式中即为Master&#xff08;主节点&#xff09;&#xff0c;控制整个集群&#xff0c;监控Worker。在YARN模式中为资源管理器ResourceManager(国内…

STM32定时器的OC比较和PWM

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. 输出比较(OC) 2. PWM 3. PWM的输出 3.1 高级定时器 3.2 通用定时器 4. PWM的输出结构 5. 代码示例 5.1 PWM.c 5.2 PWM.h 5.3 main.c 这篇文章解释了TIM定时器的内部时钟和外部时钟的使用&a…

AI图书推荐:ChatGPT写论文的流程与策略

论文一直是任何学术学位的顶峰。它展示了学生在研究领域的兴趣和专业知识。撰写论文也是一个学习经验&#xff0c;为学术工作以及专业研究角色做好准备。但是&#xff0c;论文工作总是艰苦的&#xff0c;通常是充满乐趣和创造性的&#xff0c;但有时也是乏味和无聊的。生成式人…

【kettle005】kettle访问Oracle数据库并处理数据至execl文件(已更新)

1.一直以来想写下基于kettle的系列文章&#xff0c;作为较火的数据ETL工具&#xff0c;也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下Oracle数据库相关知识体系 3.欢迎批评指正&#xff0c;跪谢一键三连&am…

IT廉连看——UniApp——样式绑定

IT廉连看——UniApp——样式绑定 一、样式绑定 两种添加样式的方法&#xff1a; 1、第一种写法 写一个class属性&#xff0c;然后将css样式写在style中。 2、第二种写法 直接把style写在class后面 添加一些效果&#xff1a;字体大小 查看效果 证明这样添加样式是没有问题的…

数字签名学习

1 基本概念 数字签名是一种加密技术&#xff0c;用于验证信息来源的身份和数据的完整性。 就是对一个东西签上自己的名&#xff1b;收到的人可以验证这东西是你发的&#xff1b;这里是用数字的方式&#xff1b; 对字符串也可以签名&#xff0c;签名以后&#xff0c;还是一个…

OpenHarmony 项目实战:智能体重秤

一、简介 本 demo 基于 OpenHarmony3.1Beta 版本开发&#xff0c;该样例能够接入数字管家应用&#xff0c;通过数字管家应用监测体重秤上报数据&#xff0c;获得当前测量到的体重&#xff0c;身高&#xff0c;并在应用端形成一段时间内记录的体重值&#xff0c;以折线图的形式…

deepflow grafana plugin 编译问题解决

修改tsconfig.js 增加"noImplicitAny": false&#xff0c;解决代码类型没有指定&#xff0c;显示Any 错误 To solve the error, explicitly set the parameters type to any, use a more specific type or set noImplicitAny to false in tsconfig.json. https://b…

白话机器学习1:分类问题中的评价指标

机器学习中的评价指标非常多&#xff0c;它们用来衡量模型的性能和预测能力。不同类型的机器学习任务可能需要不同的评价指标。以下是一些常见的评价指标&#xff0c;按照不同类型的机器学习任务分类&#xff1a; 对于分类问题&#xff1a; 准确率&#xff08;Accuracy&#…

机器学习:基于Sklearn、XGBoost框架,使用XGBClassifier、支持向量分类器和决策树分类器预测乳腺癌是良性还是恶性

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

【Camera KMD ISP SubSystem笔记】CAM SYNC与DRQ②

DRQ的作用&#xff1a; DRQ负责调度管理pipeline里的node处理逻辑(通过node之间的dependency依赖机制) 利用多线程并行处理Pipeline中并行的node&#xff0c;加快处理速度 DRQ运转流程&#xff1a; DRQ先告诉node fill dependency&#xff0c; 此时seq id 为0…

BetterDisplay Pro for Mac:显示器校准软件

BetterDisplay Pro for Mac是一款出色的显示器校准软件&#xff0c;旨在提升你的视觉体验。它提供了准确的显示器参数调整&#xff0c;包括亮度、对比度、色温和色域等&#xff0c;让你的显示器呈现更真实、清晰、细腻的图像。此外&#xff0c;软件还提供多种预设模式和自定义选…

【PyTorch 实战3:YOLOv5检测模型】10min揭秘 YOLOv5 检测网络架构、工作原理以及pytorch代码实现(附代码实现!)

YOLOv5简介 YOLOv5&#xff08;You Only Look Once, Version 5&#xff09;是一种先进的目标检测模型&#xff0c;是YOLO系列的最新版本&#xff0c;由Ultralytics公司开发。该模型利用深度学习技术&#xff0c;能够在图像或视频中实时准确地检测出多个对象的位置及其类别&…

pycharm 安装“通义灵码“并测试

过程&#xff1a;“File>setting>Plugins” 提示&#xff1a; 翻译之后&#xff1a; 点击"接受"之后&#xff0c;提示一下图片&#xff0c;点击ok 安装完成&#xff1a; 安装完"通义灵码"之后&#xff0c;需要登陆&#xff0c;登陆后测试 参考…

Springboot + MySQL + html 实现文件的上传、存储、下载、删除

实现步骤及效果呈现如下&#xff1a; 1.创建数据库表&#xff1a; 表名&#xff1a;file_test 存储后的数据&#xff1a; 2.创建数据库表对应映射的实体类&#xff1a; import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.Table…

进程的概念(2)

进程优先级 1.什么的优先级 概念&#xff1a;指定进程获取某种资源&#xff08;CPU&#xff09;的先后顺序 本质&#xff1a;优先级的本质是优先级数字的大小&#xff0c;Linux中优先级数字越小&#xff0c;优先级越高 task_struct 进程控制快-> struct -> 内部字段 -&g…

MT3608B 航天民芯代理 1.2Mhz 24V输入 升压转换器

深圳市润泽芯电子有限公司为航天民芯一级代理商 技术支持欢迎试样~Tel&#xff1a;18028786817 简述 MT3608B是恒定频率的6针SOT23电流模式升压转换器&#xff0c;用于小型、低功耗应用。MT3608B开关频率为1.2MHz&#xff0c;允许使用微小、低电平成本电容器和电感器高度不…

UE5 GAS开发P40 周期性效果,持续治疗

Periodic Gameplay Effects周期性的游戏效果 它们在一段时间内以固定的间隔重复应用到目标上。这种效果通常用于表示持续性伤害、治疗或其他影响&#xff0c;例如中毒、灼烧或回复效果。 修改GE_CrystalHeal,在Period改为每0.1秒执行一次 假如同时有三个持续时间在进行,那么这…