详解进程控制

目录

一、进程创建

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;
}

 效果展示:

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CMake使用

一、CMake 是什么 CMake 是一个跨平台的自动化构建系统&#xff0c;它使用配置文件 CMakeLists.txt 来管理软件构建过程。CMake 基于 Makefile 做了二次开发。 二、单个文件目录 # CMake 最低版本号要求 cmake_minimum_required(VERSION 3.16.3)# 工程名 project(CMakeSingle)…

Spring Boot整合Redisson的两种方式

项目场景 Spring Boot整合Redisson的两种方式&#xff0c;方式一直接使用yml配置&#xff0c;方式二创建RedissonConfig配置类。 前言 redisson和redis区别&#xff1a; Redis是一个开源的内存数据库&#xff0c;支持多种数据类型&#xff0c;如字符串、哈希、列表、集合和有序…

ZooKeeper 环境搭建详细教程之三(真集群)

ZooKeeper 搭建详细步骤之三(真集群) ZooKeeper 搭建详细步骤之二(伪集群模式) ZooKeeper 搭建详细步骤之一(单机模式) ZooKeeper 及相关概念简介 真集群搭建 搭建 ZooKeeper 真集群涉及多个步骤,包括准备环境、配置文件设置、启动服务以及验证集群状态。 以下是一个简…

java实现模板填充word,word转pdf,pdf转图片

Java实现Word转PDF及PDF转图片 在日常开发中&#xff0c;我们经常需要将文件操作&#xff0c;比如&#xff1a; 根据模板填充wordword文档中插入图片Word文档转换为PDF格式将PDF文件转换为图片。 这些转换可以帮助我们在不同的场景下展示或处理文档内容。下面&#xff0c;我将…

大型零售企业,适合什么样的企业邮箱大文件解决方案?

大型零售企业通常指的是在全球或特定地区内具有显著市场影响力和知名度的零售商。这些企业不仅在零售业务收入上达到了惊人的规模&#xff0c;而且在全球范围内拥有广泛的销售网络和实体店铺。它们在快速变化的零售行业中持续创新&#xff0c;通过实体店、电商平台等多种渠道吸…

JavaScript全套检验系统(LIS)源码C# + MVC + SQLserver + Redis 云LIS系统源码 区域医疗云LIS系统源码

JavaScript全套检验系统&#xff08;LIS&#xff09;源码C# MVC SQLserver Redis 云LIS系统源码 区域医疗云LIS系统源码 实验室信息系统&#xff08;Laboratory Information System&#xff0c;缩写LIS&#xff09;是一类用来处理实验室过程信息的软件。这套系统通常与其他信…

Dockerfile 镜像创建扩展

一、构建SSH镜像 创建dockerfile步骤&#xff1a; 1. 建立工作目录 mkdir /opt/sshd cd /opt/sshd/2.编辑Dockerfile vim Dockerfile #第一行必须指明基于的基础镜像 FROM centos:7 #作者信息 MAINTAINER this is ssh image <wwk> #镜像的操作指令 RUN yum -y update R…

微软如何打造数字零售力航母系列科普03 - Mendix是谁?作为致力于企业低代码服务平台的领头羊,它解决了哪些问题?

一、Mendix 成立的背景 Mendix的成立是为了解决软件开发中最大的问题&#xff1a;业务和IT之间的脱节。这一挑战在各个行业和地区都很普遍&#xff0c;很简单&#xff1a;业务需求通常被描述为IT无法正确解释并转化为软件。业务和IT之间缺乏协作的原因是传统的代码将开发过程限…

2路模拟音频光端机 JR-CA02

概述 JR-CA02光端机由发送机JR-CA02 Tansmitter和接收机JR-CA02 Receiver组成&#xff0c;通过一定距离长度的光纤相连接&#xff0c;传输2路Audio模拟音频&#xff08;即1路立体声&#xff09;。且每路音频分配输出。 JR-CA02光端机具有运行主要技术参数的监测功能&#xff…

最大连续1的个数 ||| ---- 滑动窗口

题目链接 题目: 分析: 题目中说可以将最多k个0翻转成1, 如果我们真的这样算就会十分麻烦, 所以我们可以换一种思路: 找到一个最长的子数组, 最多有k个0解法一: 暴力解法: 找到所有的最多有k个0的子字符串, 返回最长的解法二: 找到最长的子数组, 我们可以想到"滑动窗口算…

怎么给字符串字段加索引?

怎么给字符串字段加索引&#xff1f; 现在&#xff0c;几乎所有的系统都支持邮箱登录&#xff0c;如何在邮箱这样的字段上建立合理的索引&#xff0c;是我们今天要讨论的问题。 假设&#xff0c;你现在维护一个支持邮箱登录的系统&#xff0c;用户表是这么定义的&#xff1a; …

word 表格 文字 上下居中

问题 word 表格 文字 上下居中 详细问题 笔者进行word 文档编辑&#xff0c;对于表格中的文本内容&#xff0c;如何进行上下居中&#xff1f; 解决方案 步骤1、选中需要进行操作的单元格 步骤2、右键 → \rightarrow →点击表格属性 步骤3、依次点击单元格 → \rightar…

回溯算法解决组合问题

文章目录 回溯算法组合问题回溯算法在组合问题上的运用例题Leetcode 77. 组合Leetcode 216. 组合总和 IIILeetcode 17. 电话号码的字母组合 回溯算法 回溯算法是一种搜索方式&#xff0c;本质上其实就是穷举出所有可能&#xff0c;然后筛选出我们想要的答案。 回溯算法的效率…

mac M2 配置item2 rzsz

背景 apple m 系列处理器安装的 homebrew 跟 intel 处理器略有不同&#xff0c;其中安装目录的区别&#xff1a; m 系列处理器安装目录为 /usr/local/bin/homebrew intel 处理器安装目录为 /opt/homebrew 问题1: 卡住 产生原因&#xff1a; m 系列使用 brew install lrzs…

红米A2/A2+/POCO C51手机秒解BL+快速获取root权限+解谷歌锁刷机救砖教程

红米A2/A2/POCO C51手机是目前小米公司针对于国外用户的1个独立的品牌&#xff0c;或者和国内的红米手机都非常相似&#xff0c;几款手机由于硬件非常接近&#xff0c;我们这里将其放在一起和大家介绍而从他们的代号中我们可以得知&#xff0c;目前A2/POCO的代号为water&#x…

图像置乱加密-Arnold加密算法

置乱加密是另一种较常用的加密方法&#xff0c;现也被许多文献选用&#xff0c;置乱加密可以是以像素为单位进行全局置乱&#xff0c;该方式打乱了图像像素值的位置&#xff0c;使其图像内容失去相关性&#xff0c;达到保护的目的。也可以是以块为单位进行置乱&#xff0c;该方…

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

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

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

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

通过反汇编深入理解栈

若想更好地理解函数的多级调用、线程切换其本质&#xff0c;都需要对栈有更加深入的认识。 一、如何生成反汇编 在上图框中输入 fromelf --text -a -c --outputtest.dis xxx.axf // 把下图中的axf文件&#xff08;包括路径&#xff09;替换掉 "xxx.axf"然后编译即可…

弹性网络回归(概念+实例)

目录 前言 一、基本概念 1. 弹性网络回归的原理 2. 弹性网络回归的优点 3. 弹性网络回归的应用 4. 弹性网络回归的调参 二、实例 前言 弹性网络回归&#xff08;Elastic Net Regression&#xff09;是一种用于处理回归问题的机器学习算法&#xff0c;它结合了岭回归&…