Linux中的进程

进程控制

fork 函数

fork 函数从已存在的进程中创建新的进程,已存在进程为父进程,新创建进程为子进程

fork 的常规用法

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

fork 失败的原因

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

fork 通过写时拷贝的方式进行内容的修改:

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

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

返回子进程 pid 的原因:方便管理子进程

进程创建时,先分配 task_struct 然后分配空间

进程退出时,先回收资源,然后销毁 task_struct

僵尸进程

一个进程的关闭是先回收资源,然后再将 PCB 清理,僵尸进程就是 PCB 未被清理的进程

在系统中有个 "?" 环境变量,这个变量用来获取子进程的返回值,0 表示成功,非零表示失败,同时不同的非零值可以表示不同的失败原因,虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

进程退出的方式

  • 正常终止(可以通过echo $? 查看进程退出码):
  1. 从main返回
  2. 调用exit
  3. _exit
  • 异常退出:
  1. ctrl + c,信号终止

进程退出时,会产生退出码和退出信号,进程会将这两个值写入 PCB 中,这样就获取到了退出的信息,如果进程是异常的,将会产生退出信号,通过退出信号就能判断出异常的原因,如果没有退出信息,就可以继续查看退出码

_exit 函数

复制代码
#include<unistd.h>
void _exit(int status); 
//参数:status 定义了进程的终止状态,父进程通过wait来获取该值 
//虽然status是int,但是仅有低8位可以被父进程所用。
//所以_exit(-1)时,在终端执行$?发现返回值是255。

exit 函数

复制代码
#include <unistd.h>
void exit(int status);

exit 和 _exit 的区别

  • exit()正常终止,会执行清理操作。
  • _exit()立即终止跳过清理操作,更底层。
  • _exit() 是一个低层次系统调用,直接返回内核。
    • 它保证:
    • 立刻终止
    • 不执行任何用户态清理逻辑

子进程退出时推荐使用 _exit,因为 _exit 不会刷新缓冲区,避免了多次刷新,因为在父进程结束时还会再刷新一次

复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("Hello world\n"); // 缓冲区中暂存
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        exit(0);  // 刷新缓冲区 → 输出 "Hello world"
    } else {
        // 父进程
        wait(NULL);
    }
    return 0;  // 父进程也刷新缓冲区 → 再输出一次
}

exit() 在退出进程时会刷新缓冲区,而 _exit() 不会

exit() 是库函数,而 _exit() 是系统调用

exit最后也会调用 _exit,

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

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

return退出

return是一种常见的退出进程方法。执行return n;等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数

僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

进程等待

子进程退出后,若父进程不进行任何操作,将会产生僵尸进程,造成内存泄漏,一旦进程变成僵尸进程,将会无法被杀死

进行进程等待的原因:

父进程通过等待来解决僵尸进程的问题

父进程获取子进程的退出信息,知道子进程是什么原因退出的

wait

复制代码
#include <sys/types.h>/* 提供类型pid_t的定义*/

#include <wait.h>

pid_t wait(int *status)
返回值:
    成功返回被等待进程pid,失败返回-1。
参数:
    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

作用:进程一旦调用了wait,就会立刻阻塞自己,由wait分析当前进程中的某个子进程是否已经退出了,如果让它找到这样一个已经变成僵尸进程的子进程,wait会收集这个子进程的信息,并将它彻底销毁后返回;如果没有找到这样一个子进程,wait会一直阻塞直到有一个出现

等待就是将父进程设置为 S 状态,然后将父进程的 PCB 链接到子进程,此时就能获取到子进程的退出状态

**阻塞:**子进程没有结束,父进程执行 wait,等待某种条件的发生,此时就发生了阻塞,阻塞本质上就是进程不在调度队列上,CPU 不执行进程的代码

非阻塞等待:在等待的过程中还可以继续执行进程, 调用者立刻返回,如果事件未发生,不会停在那里等待 ,可能会导致没有等待到子进程的问题

阻塞等待:在等待过程中只能等待,不能做其他任何事情,不就绪就不返回

|----------|-----------------------------|-------------------------|
| 特性 | 阻塞等待 | 非阻塞等待 |
| 调用行为 | 卡住,直到子进程结束 | 立即返回,可能没等到子进程 |
| CPU 资源使用 | 更节省(系统调度) | 需要你自己轮询,可能浪费 CPU |
| 使用场景 | 同步执行、流程控制 | 异步程序、服务端进程池管理 |
| 接口实现 | wait(), waitpid(..., 0) | waitpid(..., WNOHANG) |

waitpid

复制代码
 waitpid(pid_t pid, int *status, int options); 
返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    返回值>0:等待成功,子进程退出,父进程成功获取退出信息
    返回值<0:等待失败
    返回值==0:检测成功,但是子进程还没退出,需要下一次重复等待
        
参数:
    pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的子进程。
    status:
        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
        WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
        WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
        若正常结束,则返回该子进程的ID。(非阻塞等待)

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞

如果不存在该子进程,则立即出错返回

waitpid 作用和 pid 等价

等待失败的情况:id 值填错

非阻塞等待+循环=非阻塞轮询,较为常用,能够允许父进程在等待的时候进行其他操作

获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

waitpid 和 wait 的区别

|----|----------------------------|-------------------------------------------------------|
| 函数 | wait | waitpid |
| 原型 | pid_t wait(int *status); | pid_t waitpid(pid_t pid, int *status, int options); |
| 功能 | 等待任意一个子进程结束 | 根据条件等待一个或多个特定子进程结束 |

waitpid 的参数:

  • pid: 可指定要等待的子进程。
    • >0: 等待指定 PID 的子进程。
    • -1: 等价于 wait,等待任意一个子进程。
    • 0: 等待与当前进程同组的任何子进程。
    • <-1: 等待特定进程组 ID 的任何子进程。
  • options: 控制行为,例如:
    • WNOHANG: 非阻塞地检查子进程是否结束。
    • WUNTRACED: 也报告已停止(但未终止)的子进程。

获取子进程 status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
复制代码
printf("child exit code:%d\n", (status>>8)&0XFF);
//退出状态在前8位,因此将后8位移除,通过和FF与操作,将退出状态中为1的保留,为0的舍去
printf("sig code : %d\n", status&0X7F );//将低7位按位与得到终止信号

进程程序替换

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

exec 函数可以让进程替换掉自己的代码和数据转而执行其他的程序

原理:exec 将被替换程序从外存中加载到内存中,将原来进程的代码和数据替换掉,task_struct 并没有被替换,因此也就没有创建新的进程

exec*系列函数在执行完毕后,后面的代码也就不会被执行了,因为已经被 exec 所执行的函数替换掉了

exec*可以不用在乎其返回值,因为一旦执行成功,后续的代码全部被替换;

一旦失败,就会继续向下执行

子进程执行 exec 时,由于进程具有独立性,因此会将原来父进程的数据和代码重新拷贝一份,然后在新拷贝的地方进行替换代码,这样就不会影响父进程,此时的父子进程就在数据结构和代码层面上彻底的分离了

复制代码
一共有 6 中 exec 系列函数
 #include<unistd.h>
int execl(const char *path, const char *arg, ...);后面可以加上多个命令的参数,但必须以NULL结尾
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[]);和execl类似,只不过是将参数放入argv[]中
int execvp(const char *file, char *const argv[]);不需要传程序的路径,因为会自动在环境变量中查找
int execve(const char *path, char *const argv[], char *const envp[]);envp为环境变量,可以传入,也可以自定义,整体把所有环境变量替换掉

命名解释: l(list) : 表示参数采用列表

v(vector) : 参数用数组

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

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

exec/exit就像call/return

一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来 返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值

相关推荐
卡戎-caryon13 分钟前
【C++】15.并发支持库
java·linux·开发语言·c++·多线程
LaoZhangGong12321 分钟前
W5500使用ioLibrary库创建TCP客户端
网络·经验分享·stm32·网络协议·tcp/ip
weixin_4342556142 分钟前
命令行快速上传文件到SFTP服务器(附参考示例)
linux·运维·服务器
掘金-我是哪吒1 小时前
分布式微服务系统架构第133集:运维服务器6年经验,高并发,大数据量系统
运维·服务器·分布式·微服务·系统架构
麟城Lincoln1 小时前
【Linux笔记】nfs网络文件系统与autofs(nfsdata、autofs、autofs.conf、auto.master)
linux·网络·笔记·nfs·autofs
Funny-Boy1 小时前
Reactor (epoll实现基础)
服务器·网络·c++
***似水流年***2 小时前
Linux任务管理与守护进程
linux·运维·服务器
tmacfrank2 小时前
Java 原生网络编程(BIO | NIO | Reactor 模式)
java·开发语言·网络
python算法(魔法师版)2 小时前
.NET NativeAOT 指南
java·大数据·linux·jvm·.net
正经教主2 小时前
【基础】Windows开发设置入门4:Windows、Python、Linux和Node.js包管理器的作用和区别(AI整理)
linux·windows·python·包管理器