【操作系统和计网从入门到深入】(三)进程控制

前言

在这里插入图片描述
这个专栏其实是博主在复习操作系统和计算机网络时候的笔记,所以如果是博主比较熟悉的知识点,博主可能就直接跳过了,但是所有重要的知识点,在这个专栏里面都会提到!而且我也一定会保证这个专栏知识点的完整性,大家可以放心订阅~

进程控制

1. 调用fork之后,OS都做了什么?

首先:进程 = 内核数据结构 + 进程代码和数据

进程调用fork。当控制转移到内核中的fork代码后,内核做:

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

可是一般而言,子进程时候没有加载的过程的,因为子进程就是从父进程中来的,不是加载而来的,也就是说,子进程没有自己的代码和数据!!!

如何保证独立性?

如果是读:那肯定没问题

如果是写:写时拷贝

问题:fork之后,是fork之后的代码共享,还是fork之前和fork之后的代码都是共享的?

2. 进程终止

终止之后,操作系统都做了什么?

常见的终止方式?

用代码,如何终止一个进程?

2.1 终止之后, 操作系统都做了什么?

释放当时申请的相关内核数据结构的对应代码和数据,本质就是释放系统资源(主要是内存)

2.2 进程常见的终止方式?

有三种情况:

  1. 代码跑完了,结果正确
  2. 代码跑完了,结果错误
  3. 代码没有跑完,崩溃了(本质是被OS发送了信号终止)
2.2.1 main() 的返回值

这个返回值不一定是0,它的本质是进程的退出码!

首先我们运行的这个myproc的父进程就是bash

我们可以在命令行中,获得最近一次的进程推出码。

所以我们可以通过退出码来判断进程运行不正确的原因。

ls也是一个程序,我们看退出码,是2,对应的就是No such file or directory

2.3 用代码终止一个进程

exit()函数,这个是C语言提供的。

_exit()函数,这个是系统调用。

C语言提供的exit() = 刷新缓冲区 + 退出。

系统调用是直接退出的。

int main()
{
    std::cout << "hello world";
    sleep(3);
    exit(1);	// 用这个, 字符串是会被打印的, 如果用_exit(1); 字符串是不会被打印的
    return 0;
}

但是记得字符串不要加上\n,因为\n有强制刷新的作用。

缓冲区是C语言维护的!

3. 进程等待

  1. 子进程退出,父进程不管子进程,子进程就要处于僵尸 状态 — 导致内存泄漏
  2. 父进程创建了子进程,是要让子进程办事儿的,那么子 进程把任务完成的怎么样?父进程需要关心吗?如果需 要,如何得知?如果不需要,该怎么处理?

两个函数:wait()waitpid()

// wait / waitpid
int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
        }
        exit(0); // 子进程结束
    }
    else
    {
        // 父进程
        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;
}

在这个代码里面,就会出现僵尸进程。因为子进程挂了之后没人去回收。

3.1 wait

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

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

上面的代码这样改:

else
{
// 父进程
    printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
    pid_t ret = wait(NULL); // 注意,这里是阻塞式等待
    if(ret > 0)
    {
        printf("父进程等待子进程成功,子进程的pid: %d\n", ret);
    }
}

3.2 waitpid

3.2.1 基本用法

waitpid(pid, NULL, 0)等价于wait(NULL)

那么现在,子进程的完成情况怎么样,我们可以通过 waitpid的第二个参数来得到!

此时我们要谈谈status的构成

status并不是按照整数来整体使用的!

而是按照比特位的方式,对32个比特位进行划分 我们只学习低16位!

首先啊,我们看这个status,有两种情况一种是正常终止,一种是被信号所杀(程序崩溃)。

3.2.2 正常终止

在正常终止的情况下,低8位全部是是0,然后次低8位是退出码。

可以实验证明。

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程
        int cnt = 5;
        while (cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid: %d\n", cnt, getpid(), getppid());
            sleep(1);
            // char *p = nullptr;
            // *p = 1; // 构造一个段错误
            cnt--;
        }
        exit(105); // 子进程结束
    }
    else
    {
        // 父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0;                      // 输出参数
        pid_t ret = waitpid(id, &status, 0); // 注意,这里是阻塞式等待
        if (ret > 0)
        {
            printf("父进程等待子进程成功,子进程的pid: %d, status: %d, 退出码: %d\n", ret, status, (status >> 8) & (0xFF));
        }
    }
    return 0;
}

用 status 右移8位,然后取低8位的就行了(异或来取)。

通过这种方法,可以获得子进程的退出码。

3.2.3 被信号所杀

注意,如果是被信号所杀,那么这个进程是没有正常结束的,那么他的退出码没有任何意义!

else
{
    // 父进程
    printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
    int status = 0;                      // 输出参数
    pid_t ret = waitpid(id, &status, 0); // 注意,这里是阻塞式等待
    if (ret > 0)
    {
        printf("父进程等待子进程成功,子进程的pid: %d, status: %d, 信号: %d, 退出码: %d\n", ret, status, (status & 0x7F), (status >> 8) & (0xFF));
    }
}

通过取前7位,就能获得信号。

然后我们上面子进程弄一个段错误,让os给我们发个信号。

这样我们就验证成功了。

4. 解决一些问题

5. 继续理解waitpid的参数

第一个参数id:

  1. id > 0 表示等待指定进程
  2. id == 0 稍后再说
  3. id == -1 表示等待任意一个子进程退出,等价于wait()

其实按照上面的方法,如果我们要提取status里面的东西,还需要懂位运算,太麻烦了,其实操作系统给我们提供了宏的。

WIFEXITEDWEXITSTATUS

else
{
    // 父进程
    printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
    int status = 0;                      // 输出参数
    pid_t ret = waitpid(id, &status, 0); // 注意,这里是阻塞式等待
    if (WIFEXITED(status)) // WIFEXITED 为真表示正常退出
    {
        printf("子进程执行完毕,子进程的退出码: %d\n", WEXITSTATUS(status));
    }
    else
    {
        printf("子进程异常退出: %d\n", WIFEXITED(status));
    }
}

6. waitpid的非阻塞等待

现在我们知道,当父进程在等待子进程死的时候,父进程是 阻塞式等待,也就是说,父进程在等待的时候啥都没干,那 么此时,如果我们想让父进程继续去做一些事情呢? 此时我们需要设置第三个参数, 第三个参数默认为0,表示阻塞等待! WNOHANG表示非阻塞等待。

非阻塞等待:

如果父进程检测子进程的退出状态,发现子进程没有退出,我们的父进程通过 调用waitpid来进行等待。 如果子进程没有退出,我们waitpid这个系统调用立马返回!

那么非阻塞调用的时候,直接就返回了。

那我们怎么知道子进程的状态呢? 我们会每隔一段时间去检测一下子进程的状态,如果监测到子进程结束,就释放子进程 这个叫做 — 基于非阻塞调用的轮询检测方案。

为什么要重点学习阻塞和非阻塞?

因为我们未来编写代码的内容,大部分都是网络代码,大部分都是IO,要不断面临阻塞和非阻塞。

下面是一个小demo

如果以后想让父进程闲的时候去搞一些其他事情,只需要向Load()里面去注册就行了。

#include <iostream>
#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <assert.h>

/*
    这里是非阻塞等待线程的一个小demo应用
*/

typedef void (*handler_t)();     // 函数指针类型
std::vector<handler_t> handlers; // 这个数组里面存放函数指针

void fun_one() { std::cout << "task 1 doing ... " << std::endl; }
void fun_two() { std::cout << "task 2 doing ... " << std::endl; }

// 设置对应的回调
void Load()
{
    // 注册
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

int main()
{
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        // 子进程
        int cnt = 5;
        while (cnt--)
        {
            printf("我是子进程, 我的pid: %d, cnt: %d\n", getpid(), cnt);
            sleep(1);
        }
        exit(11); // 仅仅用来测试,如果正常来说是要返回0的。
    }
    else
    {
        int quit = 0;
        while (!quit)
        {
            int status = 0;
            pid_t res = waitpid(-1, &status, WNOHANG); // 非阻塞等待
            if (res > 0)
            {
                // 等待成功,子进程退出了
                printf("等待子进程成功, 退出码: %d\n", WEXITSTATUS(status));
                quit = 1;
            }
            else if (res == 0)
            {
                // 等待成功,但是子进程没有退出!
                printf("子进程还在执行当中,父进程可以等一等,处理一下其他事情\n");
                if (handlers.empty())
                {
                    Load();
                }
                for(auto it : handlers)
                {
                    it(); // 执行已经注册好的事情
                }
            }
            else
            {
                // 等待失败,其实就是waitpid等待失败,比如我们id写错了,没有id这个进程,一般才会出现这个问题
                printf("waitpid失败\n");
                quit = 1;
            }
            sleep(1);
        }
    }
    return 0;
}

7. 进程替换

进程替换,有没有创建新的进程?

没有!因为pcb这些结构没变!

如何理解所谓的将程序放入内存 中?

本质就是加载!

有六种exec开头的函数。

7.1 函数的基本用法

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

可变参数列表。

命令行怎么执行的,就怎么一个一个写上去就行了,最后一个参数必须是NULL,表示参数传递完毕!

int main()
{
    std::cout << "begin to run the main function" << std::endl;
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    std::cout << "finish to run the main function" << std::endl;
    return 0;
}

最后一句没有被执行,因为替换之后就回不去了!

7.2 这些函数需要返回值吗?

调用失败才会有返回值!

为什么?

因为你调用成功了,你程序都换了,返回值有什么用呢?

而且!

我们也根本不用对返回值做判断,因为如果调用失败了,我们在后面直接exit(1)不就好了?

如果调用成功了,这个exit(1)也不会呗执行。

所以我们一般创建子进程去调用这些进程替换,这样也不会影响父进程了。

int main()
{
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        // 子进程
        std::cout << "子进程开始运行 ... " << std::endl;
        execl("/usr/bin/ls", "ls", "-al", NULL);
        exit(1); // 如果调用失败,直接退出
    }
    else
    {
        // 父进程
        std::cout << "父进程开始运行, pid: " << getpid() << std::endl;
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0)
        {
            std::cout << "wait success, exit code: " << WEXITSTATUS(status) << std::endl;
        }
    }
    return 0;
}

7.3 看下其他的替换函数

execl其实和execv是一样的。

l的意思就是list所有的参数,就是可变参数嘛。

然后v的意思就是vector,就是把所有的参数放到数组里面才放进去。

execv第二个参数char *const argv[]是一个指针数组。

这两个函数除了传参的区别之外,没有任何的区别。

char* const argv[] = {"ls", "-al", NULL};
execv("/usr/bin/ls", argv);

execlp 的 p 我们可以理解成 我会自己在环境变量PATH中进行查找,你不用告诉我你要执行的程序在哪里?

execlp("ls", "ls", "-al", NULL);

第三个要稍微说一下。

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

**我们可以通过execle(execvpe)向替换的程序传递环境变量!**我们就不演示了。

7.4 演示:用一个C++程序去调用一个Python程序

// 用一个C++去调用一个python
int main()
{
    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        // 子进程
        std::cout << "child proc working ..." << std::endl;
        execl("/usr/bin/python", "python", "hello_python.py", NULL);
        std::cout << "called python failed!" << std::endl;
        exit(1);
    }
    else
    {
        // 父进程
        std::cout << "father proc working, pid: " << getpid() << std::endl;
        int status = 0;
        pid_t id = waitpid(-1, &status, 0); // 阻塞等待
        if (id > 0)
        {
            std::cout << "wait success, exit code: " << WEXITSTATUS(status) << std::endl;
        }
    }
    return 0;
}
print("hello python! I am a python program!")

7.5 总结

以上我们学的6个程序替换接口,严格意义上并不是系统调用,而严格意义上的系统调用只有一个,叫做execve。

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

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

相关文章

【大数据-Hadoop】从入门到源码编译-概念篇

【大数据-Hadoop】从入门到源码编译-概念篇 Hadoop与大数据生态&#xff08;一&#xff09;Hadoop是什么&#xff1f;&#xff08;二&#xff09;Hadoop组成1. HDFS1.1 NameNode&#xff08;nn&#xff09;1.2 DataNode&#xff08;dn&#xff09;1.3 Secondary NameNode&#…

如雨后春笋般层出不穷的人工智能,究竟可以为我们的生活带来些什么?

似乎是从chatgpt爆火以后&#xff0c;各种各样的和AI、人工智能有关的产品层出不穷&#xff0c;似乎只有带有人工智能&#xff0c;才能体现一个产品的功能之强大&#xff0c;才能在众多产品中具有一定的竞争力&#xff0c;那么这样的现象会给我们的生活带来什么影响呢&#xff…

如何用scratch画正多边形

各边相等&#xff0c;各角也相等的多边形叫做正多边形。 正多边形的外接圆的圆心叫做正多边形的中心。 正多边形的外接圆的半径叫做正多边形的半径。 中心到圆内接正多边形各边的距离叫做边心距。 正多边形各边所对的外接圆的圆心角都相等&#xff0c;这个圆心角叫做正多边…

各行各业模板ppt模板打包下载

下载地址 https://download.csdn.net/download/douluo998/88624912 超多ppt模板 136-高级古风PPT 135-高端艺术PPT 134-高端A4竖版PPT 133-露营活动PPT 132-雷锋主题PPT 131-退休欢送会PPT 130-转正述职报告PPT 129-谷雨PPT 128-课堂互动游戏PPT 127-读书分享PPT -2023-07-22 1…

多线程 (上) - 学习笔记

前置知识 什么是线程和进程? 进程: 是程序的一次执行,一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间&#xff0c;一个进程可以有多个线程&#xff0c;比如在Windows系统中&#xff0c;一个运行的xx.exe就是一个进程。 线程: 进程中的一个执行流&#xff0…

计算机操作系统原理分析期末复习

一、理解与识记 三种基本的OS类型及各自的特点&#xff1a; 批处理系统&#xff08;内存同时存放几个作业。优点&#xff1a;资源利用率高、作业吞吐量大、系统开销小&#xff1b;缺点&#xff1a;用户无交互性、作业平均周转时间长&#xff09;、分时系统&#xff08;时间片技…

后端项目全局异常处理-使用RuntimeException自定义异常异常分类简单举例

接上篇&#xff1a;后端项目操作数据库-中枢组件Service调用Mapper 自定义异常&#xff1a; 手动抛出异常&#xff0c;为了后续统一捕获&#xff0c;需要异常自定义&#xff1b; 如&#xff1a;当使用抛出异常的方式表示“操作失败”时&#xff0c;为了后续统一捕获&#xff0c…

信息过载的反思

在今天微信、短视频、图文不停的密集的信息轰炸之下&#xff0c;你“察觉”到你的精力不济没有&#xff1f;你时常会觉得耳鸣、目涩&#xff0c;注意力无法集中&#xff1b;你懒于记忆&#xff0c;甚至爱人的手机号都想不起来&#xff0c;习惯于用移动电话找人名&#xff0c;不…

Docker - Android源码编译与烧写

创建源代码 并挂载到win目录 docker run -v /mnt/f/android8.0:/data/android8.0 -it --name android8.0 49a981f2b85f /bin/bash 使用 docker update 命令动态调整内存限制&#xff1a; 重新运行一个容器 docker run -m 512m my_container 修改运行中容器 显示运行中容器 d…

使用飞书自定义机器人发送消息

使用飞书机器人可以很方便的获取自动化任务的反馈&#xff1a; 在群里创建一个机器人&#xff1a; 记住下面的 webhook地址&#xff0c;这个是标识机器人的唯一ID&#xff0c;比如它的webhook地址是&#xff1a;"https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxx-a…

【1.计算机组成与体系结构】流水线技术

目录 1.流水线的定义2.相关参数计算2.1 流水线计算公式2.2 流水线的吞吐率2.3 流水线加速比计算 3.超标量流水线 1.流水线的定义 流水线是指在程序执行时多条指令重叠进行操作的一种准并行处理实现技术。各种部件同时处理是针对不同指令而言的&#xff0c;它们可同时为多条指令…

STM32——超声波传感器

需求&#xff1a; 使用超声波测距&#xff0c;当手离传感器距离小于 5cm 时&#xff0c; LED1 点亮&#xff0c;否则保持不亮状态 接线&#xff1a; 定时器配置&#xff1a; 使用 TIM2 &#xff0c;只用作计数功能&#xff0c;不用作定时。 将 PSC 配置为 71 &#xff0c;…

笔记 - 现代嵌入式芯片封装识读

0.引用&#xff1a; 配图、资料并非一处采集&#xff0c;我不太容易找到图片的原始链接。这里的图片仅作示例&#xff0c;无商业用途。如果涉及侵权&#xff0c;请随时联系。谢谢&#xff01; PCB封装欣赏了解之旅&#xff08;下篇&#xff09;—— 常用集成电路_ufqfpn封装…

什么是Z-Wave,技术特点,各国支持的频段

1.1 背景 Z-Wave是一种无线通信的协议&#xff0c;主要应用于智能家居网络&#xff0c;而其Z-Wave联盟主要是推动家庭自动化领域采用Z-Wave协议&#xff0c;其联盟成员都是智能家居领域非常有名的厂商&#xff0c;基本上覆盖了全球。 2.1 技术特点 低功耗、高可…

『App自动化测试之Appium应用篇』| 元素定位工具Appium-Inspector从简介、安装、配置到使用的完整攻略

『App自动化测试之Appium应用篇』| 元素定位工具Appium-Inspector从简介、安装、配置到使用的完整攻略 1 Appium-Inspector简介2 Appium Desktop中的Appium-Inspector3 安装Appium-Inspector4 Appium-Inspector网页版5 Appium-Inspector界面说明5.1 Appium Server配置5.2 Selec…

飞致云与上海吉谛达成战略合作,获得Gitea企业版中国大陆地区独家代理权

2023年12月13日&#xff0c;中国领先的开源软件提供商FIT2CLOUD飞致云宣布与上海吉谛科技有限公司&#xff08;以下简称为上海吉谛&#xff09;正式达成战略合作&#xff0c;FIT2CLOUD飞致云获得上海吉谛旗下代码托管平台Gitea企业版中国大陆地区独家代理权。 Gitea项目&…

使用 Pnpm 和 Vite 构建 Vue 项目

文章目录 本地 Node 环境安装 Pnpm 包管理工具使用 Vite 创建 Vite 官网&#xff1a;https://cn.vitejs.dev/ 本地 Node 环境 首先&#xff0c;确保已经安装了 Node.js 和 npm。可以在命令行中运行 node -v 和 npm -v 来检查它们是否已经正确安装&#xff1a; 安装 Node.js 通…

【Git 小妙招】走进 Git 的分支管理(万字图文讲解)

文章目录 前言1. 理解分支2. 创建分支3. 切换分支4. 合并分支5. 删除分支6. 合并冲突7. 分支管理策略7.1 一个简单的分支策略(仅参考) 8. bug 分支9. 删除临时分支总结 前言 本文开始介绍 Git 的杀手级功能之⼀&#xff1a;分⽀。本文涉及分⽀创建&#xff0c;切换&#xff0c…

2. 基础数据结构-数组

2. 基础数据结构-数组 2.1 概念 数组是一种数据结构&#xff0c;它是一个由相同类型元素组成的有序集合。在编程中&#xff0c;数组的定义是创建一个具有特定大小和类型的存储区域来存放多个值。数组可以是一维、二维或多维的。每个元素至少有一个索引或键来标识。 2.2 数组特…

Leetcode—113.路径总和II【中等】

2023每日刷题&#xff08;五十七&#xff09; Leetcode—113.路径总和II 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* …