前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >详谈 Linux进程控制(看这一篇就够了)

详谈 Linux进程控制(看这一篇就够了)

作者头像
用户11317877
发布2024-12-20 08:46:04
发布2024-12-20 08:46:04
13500
代码可运行
举报
文章被收录于专栏:学习学习
运行总次数:0
代码可运行

🚀一. 前言

本文将系统介绍进程控制的基本要素,包括进程创建, 进程终止, 进程等待, 进程替换等方面。深入理解进程创建的相关知识, 帮助更好的构建知识架构!

博客主页:酷酷学!!!感谢关注!

🚀二. 进程创建

✨1. fork函数的认识

在linux中fork函数非常重要, 它从已存在的进程中创建一个新的进程, 新进程为子进程,而原进程为父进程。

代码语言:javascript
代码运行次数:0
运行
复制
#include<unistd.h>
pid_t fork(void);
返回值:子进程中返回0, 父进程中返回子进程id, 出现错误则返回-1

进程调用fork,当控制转移到内核中的fork代码后, 内核会做下面几件事情:

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

当一个进程调用fork()之后,就有两个二进制代码相同的进程。而且他们都运行到相同的地方。但每个进程都将可以开始他们自己的旅程。

这里我们看到了三行输出, 一行before, 两行after,父进程先打印before消息,然后它又打印after,另一个after消息有子进程打印的,但是子进程没有打印before,为什么呢?

所以, fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行,注意,fork之后,谁先执行由调度器决定。

✨2. fork函数的返回值

子进程返回0,父进程返回的是子进程的pid。

为什么有两个返回值, 因为fork之后是两个不同的进程, 而返回值也是给不同的进程。

代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  

int main() {  
    pid_t pid = fork();  // 执行 fork()  

    if (pid < 0) {       // 处理 fork 失败的情况  
        perror("fork failed");  
        exit(EXIT_FAILURE);  
    } else if (pid == 0) { // 子进程执行  
        printf("This is the child process (PID: %d)\n", getpid());  
    } else {              // 父进程执行  
        printf("This is the parent process (PID: %d, Child PID: %d)\n", getpid(), pid);  
    }  

    return 0;            // 所有进程执行完毕  
}

✨3. 写时拷贝

通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,如下图:

所以数据不修改时,代码共享是因为子进程拷贝了父进程的mm_struct和页表,物理内存是一样的。

那系统是怎么知道要写时拷贝的呢具体是怎么做到呢?

首先第一步, 系统会在一开始就把权限设置为了只读权限, 如果要发生写入,则会引发系统错误, 导致缺页中断, 这是系统会进程判断, 如果是要发生写时拷贝,则系统会申请内存,然后进行拷贝一份,再修改子进程的页表将物理地址修改为实际的物理地址,然后再恢复到只读权限,你的写入操作不是目标区域进行覆盖。比如count++。

✨4. fork的常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段, 例如, 父进程等待客户端请求,生成子进程来处理请求。

一个进程要执行一个不同的程序, 例如子进程从fork返回后,调用exec函数。

✨5. fork调用失败的原因

  1. 系统中有太多的进程。
  2. 实际用户的进程数量超过了限制。

🚀三. 进程终止

本质:释放系统资源, 就是释放进程申请的相关内核数据结构和对应的代码和数据。

✨1. 进程退出的场景

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

✨2. 进程常见的退出方法

🐨2.1 从main返回

除了main函数的返回值表示进程结束,其它函数的return都表示函数结束。

🛩️2.1.1 错误码

main函数的返回值是返回给父进程或者系统的,命令行中获取最近一个进程的返回值我们可以使用echo $?来获取, 如下图所示:

对于返回值,0表示成功, 非0表示错误,为什么会失败呢?系统提供了不同的错误码信息记录了错误的原因, 也可以自己约定错误码。

那么什么是错误码呢? 举个例子:

如果想要查看错误码, 我们可以使用errno函数, 使用man可以查看命令详情。

如果想知道具体的错误内容, 可以使用strerror函数,参数传递错误码。

举个例子:

我们可以将错误码和对应的错误信息进行打印:

例如:

🐨2.2 exit函数

在代码的任何地方, 让进程直接结束。参数就是返回的错误码。

如果使用exit, 如果缓冲区有数据, 则会被刷新出来。

🐨2.2 _exit函数

是系统层的进程终止调用,

如果使用_exit, 缓冲区的数据则不会被刷新出来。

🐨2.4 缓冲区问题补充(为什么_exit不刷新缓冲区)

exit属于是语言级别的,在三号手册, 而_exit是系统级别的,在二号手册。

_exit本质上是系统调用

所以我们上面的_exit实际上是绕过了语言层, 直接进行了系统调用, 而刚刚的缓冲区是语言级别的, fflush也是语言级别的。

🚀四. 进程等待

首先我们可以查看一下fork的返回值, 如果fork失败, 则错误码会被设置。

✨1. wait和waitpid等待回收子进程

一般而言, 父进程创建的子进程, 父进程就要等待子进程进行回收, 如果子进程一直不退出, 则父进程就会阻塞在wait内部。

wait的作用,等待任意的子进程(参数可以传nullptr表示不获取status)

常用: waitpid的作用:第一个参数 pid>0表示等待指定的一个进程,pid == -1表示等待任意一个子进程

看一下他们的返回值, 如果等待成功则返回对应的子进程,如果等待失败则返回-1.

举个例子:

等待任何一个子进程

当然, 我也可以修改id的参数,比如更换为刚刚子进程id,这里就不展示了。

waitpid的第二个参数,它会帮助父进程获取子进程的退出信息,通过参数的方式给我们带出来。输出型参数。

但是这里的退出信息却是256,为什么不是1呢?

其实,status这个参数包含的信息并不只是退出码,它的本质是一个位图, 它的结构中前八位是退出状态,有256种状态 ,低七位是终止信号, 还有一个标志位。

如果想要提取退出状态则需要进行位运算,如下

小问题: 那我们可不可以不使用status来获取状态码,而是用一个全局变量呢?

不可以, 因为进程具有独立性,子进程修改父进程看不到,会发生写时拷贝。

✨2. 重谈进程退出

进程退出分为三种:

  1. 代码跑完, 结果正确, return 0
  2. 代码跑完, 结果错误, return !0
  3. 进程异常导致退出(OS提前使用终止信号把进程终止了)

如果进程没有异常,则终止信号是0,所以上面出现的256就得到解释了,低七位都是0。

终止信号:

也可以进行终止信号的提取:

实例,让子进程对数据进行备份

其中WEXISTATUS这个宏 可以直接让我们从错误信息中提取出退出码。

✨3. 阻塞与非阻塞

waitpid的第三个参数就是关于阻塞等待与非阻塞等待

首先waitpid的返回值, 如果>0表示,返回目标进程pid, 如果 == 0, 等待成功,但是子进程没有退出, <0 等待失败.

参数如果传0表示阻塞等待, 如果传WNOHANG表示非阻塞等待。

举个例子:

🚀五. 进程程序替换

fork()之后, 父子各自执行父进程代码的一部分如果子进程想执行一个全新的程序呢? 进程的程序替换来完成这个功能!

程序替换是通过特定的接口, 加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

✨1. 替换原理

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

✨2. 替换函数

那么下面就让我们看看有哪些调用接口。

其实有七种调用接口, 六种是c标准库提供的, 还有一个是系统层的

代码语言:javascript
代码运行次数:0
运行
复制
#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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
🐨2.1 函数解释
  1. 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  2. 如果调用出错则返回-1
  3. 所以exec函数只有出错的返回值而没有成功的返回值。

举个例子:

🐨2.2 命名理解

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

• l(list): 表示参数采用列表 • v(vector): 参数用数组 • p(path): 有p自动搜索环境变量PATH • e(env): 表示自己维护环境变量

示范:

代码语言:javascript
代码运行次数:0
运行
复制
#include <unistd.h>
int main()
{
 char *const argv[] = {"ps", "-ef", NULL};
 char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 execl("/bin/ps", "ps", "-ef", NULL);
 // 带p的,可以使⽤环境变量PATH,⽆需写全路径 
 execlp("ps", "ps", "-ef", NULL);
 // 带e的,需要⾃⼰组装环境变量 
 execle("ps", "ps", "-ef", NULL, envp);
 execv("/bin/ps", argv);
 
 // 带p的,可以使⽤环境变量PATH,⽆需写全路径 
 execvp("ps", argv);
 // 带e的,需要⾃⼰组装环境变量 
 execve("/bin/ps", argv, envp);
 exit(0);
}

最后推荐两本书籍:

《深入理解计算机系统》与《程序员的自我修养》


🚀六. 总结

进程控制是操作系统中的一个重要主题,主要涉及如何管理和调度进程以确保计算机系统的高效运行。

如果感觉本篇博客对你有帮助的话就点个关注吧~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🚀一. 前言
  • 🚀二. 进程创建
    • ✨1. fork函数的认识
    • ✨2. fork函数的返回值
    • ✨3. 写时拷贝
    • ✨4. fork的常规用法
    • ✨5. fork调用失败的原因
  • 🚀三. 进程终止
    • ✨1. 进程退出的场景
    • ✨2. 进程常见的退出方法
      • 🐨2.1 从main返回
      • 🐨2.2 exit函数
      • 🐨2.2 _exit函数
      • 🐨2.4 缓冲区问题补充(为什么_exit不刷新缓冲区)
  • 🚀四. 进程等待
    • ✨1. wait和waitpid等待回收子进程
    • ✨2. 重谈进程退出
    • ✨3. 阻塞与非阻塞
  • 🚀五. 进程程序替换
    • ✨1. 替换原理
    • ✨2. 替换函数
      • 🐨2.1 函数解释
      • 🐨2.2 命名理解
  • 🚀六. 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档