Liunx 操作系统 进程控制(上)

一. 进程创建

1.fork函数

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

例如:

调用 fork() 之前:

复制代码
        父进程
          │
          │ 执行
          ▼
       fork()

调用成功以后:

复制代码
              fork()
                 │
          ┌──────┴──────┐
          ▼             ▼
        父进程          子进程
        PID=100         PID=101

父子进程会从 fork() 之后的位置继续执行。

**2.**fork函数返回值

子进程返回0

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

为什么这样设计?

因为父进程可能有很多子进程:

复制代码
             父进程
           /    |    \
          /     |     \
       子进程  子进程  子进程
        101     102     103

父进程需要知道:我刚刚创建出来的这个子进程到底是谁?

所以父进程得到:101

也就是子进程 PID。

而子进程只需要知道:我是子进程。

所以直接返回:0

因此可以利用返回值让父子进程执行不同代码:

复制代码
pid_t pid = fork();

if (pid == 0)
{
    // 子进程
}
else if (pid > 0)
{
    // 父进程
}
else
{
    // fork失败
}

为什么一个 fork() 函数会有两个返回值?

这里最容易产生误解。

不是一个函数执行了一次,然后返回了两次。

而是fork() 调用一次,但是调用成功以后,系统中存在两个进程,并且父子进程都会从 fork() 返回的位置继续执行。

例如:

复制代码
printf("A\n");

fork();

printf("B\n");

执行过程:

复制代码
fork()之前

        一个进程
           │
           ▼
        printf A
           │
           ▼
         fork()
           │
      ┌────┴────┐
      ▼         ▼
    父进程     子进程
      │         │
      ▼         ▼
 printf B    printf B

所以最终:

复制代码
A
B
B

关键理解

fork() 本身不是"返回两次"。

而是:

复制代码
一个进程
   ↓ fork()
两个进程
   ↓
两个进程分别从 fork() 后面继续执行

所以看起来就像 fork() 有两个返回值。

为什么同一个 pid 既可以等于 0,又可以大于 0?

复制代码
pid_t pid;

pid = fork();

printf("%d\n", pid);

pid 是每个进程自己的变量。

fork() 后,父子进程各自拥有自己的进程地址空间。

所以实际上变成:

复制代码
父进程:

pid = 101


子进程:

pid = 0

虽然它们最开始是从同一份代码复制出来的,但是已经是两个独立的进程。

因此:

复制代码
父进程看到的 pid = 101

子进程看到的 pid = 0

并不存在:同一个变量同时既是 0 又是 101

而是:父进程中的 pid 和子进程中的 pid 是两个不同进程里的变量。

fork之后谁先执行?

不确定。

例如:

复制代码
pid = fork();

printf("hello\n");

可能:

复制代码
父进程 hello
子进程 hello

也可能:

复制代码
子进程 hello
父进程 hello

因为 fork() 返回之后,由 Linux 的进程调度器决定谁先获得 CPU。

所以:不要认为父进程一定先执行,也不要认为子进程一定先执行。

3.写时拷贝

fork() 后,父子进程虽然拥有各自独立的地址空间,但一开始并不会立即把所有数据真的复制一份。

可以简单理解成:

复制代码
父进程
  │
  ├──────────┐
  │          │
  ▼          ▼
      共同使用物理内存

如果父子进程只是读取:

复制代码
printf("%d", x);

可以暂时共享。

但是如果某一个进程修改:

复制代码
x = 100;

系统才真正复制对应的内存:

复制代码
             原来的内存
                 │
          ┌──────┴──────┐
          ▼             ▼
       父进程          子进程
       自己一份        自己一份

这就是写时拷贝

如果 fork() 一开始就把父进程所有内存全部复制:

复制代码
父进程 1GB
   ↓ fork
子进程再复制1GB

非常浪费。

很多时候子进程根本不会修改这些数据。

所以 Linux 采用:先共享,谁写谁复制。

这样可以节省内存,提高效率。

**4.**fork常规用法

① 创建子进程处理任务

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

例如服务器:

复制代码
              父进程
          等待客户端请求
                 │
               fork()
             /       \
            ▼         ▼
         父进程      子进程
        继续等待    处理请求

这就是很典型的服务器模型。


② fork + exec 执行另一个程序

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

例如:

复制代码
父进程
   │
   ▼
 fork()
   │
   ▼
子进程
   │
   ▼
 exec()
   │
   ▼
运行另一个程序

这也是 Linux 中非常重要的进程创建方式。

**5.**fork 失败的原因

正常情况下:

复制代码
pid = fork();

可能出现三种情况:

复制代码
pid < 0    → fork失败

pid == 0   → 当前是子进程

pid > 0    → 当前是父进程,pid是子进程PID

fork() 失败常见原因:

  1. 系统进程数量达到限制
  2. 当前用户的进程数量达到限制
  3. 系统资源不足,例如内存不足

二.进程终止

进程终止,本质上就是进程退出,并释放它占用的系统资源。

**1.**进程退出场景

① 代码运行完毕,结果正确

例如:

复制代码
int main()
{
    printf("hello\n");
    return 0;
}

正常运行:

复制代码
hello

然后程序结束。

退出码:

复制代码
0

表示正常。


② 代码运行完毕,但是结果不正确

例如程序自己检测到了错误:

复制代码
int main()
{
    printf("发生错误\n");
    return 1;
}

程序虽然正常执行到了 return,但是告诉 Shell:我这次运行有问题。

所以:

复制代码
退出码 = 1

③ 代码异常终止

例如:

复制代码
Ctrl + C

会向程序发送:

复制代码
SIGINT

程序收到信号后通常会终止。

所以:

复制代码
正常终止 ≠ 异常终止

2.Linux 中进程正常退出的三种方式

最重要的三个:

复制代码
① return
② exit()
③ _exit()

退出码

退出码就是:程序结束时给操作系统 / Shell 留下的一个数字,用来表示程序执行结果。

最常见:

复制代码
0      → 成功
非0    → 通常表示出现问题

例如:

复制代码
int main()
{
    return 0;
}

执行:

复制代码
./a.out
echo $?

得到:

复制代码
0

为什么 echo $? 可以查看退出码?

这里的 $? 是 Shell 中一个特殊变量。

复制代码
echo $?

意思就是:查看上一条命令的退出状态。

例如

复制代码
./a.out
echo $?

如果程序:

复制代码
return 0;

那么:

复制代码
0

如果:

复制代码
return 10;

那么:

复制代码
10

五、为什么退出码通常用 0 表示成功?

这是 Linux/Unix 中约定俗成的规则:

复制代码
0     → 成功
非0   → 失败/异常/其他状态

所以写程序的时候经常看到:

复制代码
return 0;

意思就是:程序正常结束,没有错误。

而:

复制代码
return 1;

通常表示:程序执行过程中出现了某种错误。


退出码不是"错误码"的严格同义词

退出码本质上是程序结束时提供给父进程/Shell 的状态值。

例如:

复制代码
return 100;

不一定意味着:错误100

它只是:程序退出时返回了 100。

至于 100 具体代表什么,是程序自己定义的。

3._exit函数

作用:直接终止当前进程。

例如:

复制代码
#include <unistd.h>

int main()
{
    _exit(0);
}

程序直接退出。

为什么 _exit(-1) 最后是 255?

虽然:

复制代码
_exit(int status);

参数是 int

复制代码
int = 32位

但是进程退出状态中,通常只有低 8 位用于传递退出码。

也就是:

复制代码
0 ~ 255

_exit(-1)

-1 的低 8 位:

复制代码
11111111

换成无符号数就是:

复制代码
255

所以:

复制代码
_exit(-1);

然后:

复制代码
echo $?

得到:

复制代码
255

记住退出码通常只看低 8 位,所以范围可以理解为 0~255。

exit()_exit() 的区别

两者都会让进程终止,但是 exit() 在真正终止之前还会做一些清理工作。


_exit()

可以简单理解:

复制代码
_exit()
   ↓
直接进入进程终止流程

exit()

可以理解成:

复制代码
exit()
  ↓
执行清理函数
  ↓
刷新/关闭标准 I/O 流
  ↓
调用 _exit()
  ↓
进程终止

所以:exit() 最终也会调用 _exit(),但是在调用 _exit() 之前会做额外的清理工作。

为什么 printf("hello") + exit() 能看到 hello?

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

int main()
{
    printf("hello");
    exit(0);
}

注意:

复制代码
printf("hello");

这里没有:

复制代码
\n

所以 hello 可能还留在 stdio 的输出缓冲区里,没有立即写到终端。

但是:

复制代码
exit(0);

会处理标准 I/O 流,并刷新缓冲区。

所以最终能看到:

复制代码
hello

为什么换成 _exit() 后 hello 消失了?

代码:

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

int main()
{
    printf("hello");
    _exit(0);
}

流程:

复制代码
printf("hello")
      ↓
hello可能还在stdio缓冲区
      ↓
_exit(0)
      ↓
直接终止
      ↓
没有通过exit进行stdio缓冲区刷新

因此终端可能什么都看不到。

也就是说:

复制代码
exit()
   ↓
刷新stdio缓冲区
   ↓
hello显示

而:

复制代码
_exit()
   ↓
不负责这种stdio层面的清理
   ↓
hello可能没显示

return 为什么也能退出进程?

例如:

复制代码
int main()
{
    printf("hello\n");

    return 0;
}

这里:

复制代码
return 0;

会结束 main()

main() 是程序的入口函数。

C 运行时环境会把:

复制代码
return 0;

对应到进程退出状态。

可以简单记成:

复制代码
main中的 return n
        ↓
相当于
exit(n)

所以return n 等同于 exit(n)

复制代码
相关推荐
傲世仙尊1 小时前
从.o到ELF-静态库动态库与可执行文件的生命周期
linux
指尖的爷1 小时前
【地狱级难度】ubuntu系统GOCV编译安装
linux·运维·ubuntu
xx~t1 小时前
嵌入式——ARM——汇编2
linux·汇编·arm开发·嵌入式硬件·启动代码
智购科技自动贩卖机1 小时前
自动售货机嵌入式系统时钟同步与时间管理实战:从RTC校准到断网时间保持的工程实践
大数据·linux·数据库·人工智能·yolo
BizzZ_1 小时前
Linux(2)——权限
linux
j7~1 小时前
【C++微服务项目开发脚手架】(环境篇)虚拟机 + Docker + MySQL/Redis/RabbitMQ/ES/etcd/FastDFS 全套配齐
linux·c++·ubuntu·docker·vmware·项目开发·微服务脚手架
Biomamba生信基地1 小时前
安全访问:FutureTerminal之Jupyter配置及Linux终端、Python与R环境访问
linux·远程连接·生物信息学
M78佐菲1 小时前
ARM学习笔记(三)
linux·arm开发·笔记·嵌入式硬件·学习
邪修king1 小时前
Re:Linux 系统篇(三十一):库的制作与原理Chapter2:静态链接与程序加载 —— 从磁盘 ELF 到运行中进程的完整旅程
android·linux·运维·开发语言