四、进程控制(进程创建与终止)

1. 进程创建

1-1 fork函数初识

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

cpp 复制代码
#include

pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程id,出错返回-1

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

• 分配新的内存块和内核数据结构给子进程

• 将父进程部分数据结构内容拷贝至子进程

• 添加子进程到系统进程列表当中

• fork返回,开始调度器调度

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

fork 之前:一个进程,一条执行流。fork 之后:两个进程,两条执行流,从同一行代码开始各跑各的。


1-2 fork 返回值

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

int main(void)
{
    pid_t pid;
    printf("Before: pid is %d\n", getpid());
    if ((pid = fork()) == -1) perror("fork()"), exit(1);
    printf("After: pid is %d, fork return %d\n", getpid(), pid);
    sleep(1);
    return 0;
}

看刚才跑出来的结果:

复制代码
Before: pid is 165611                           ← 父进程 fork 之前,只执行一次
After: pid is 165611, fork return 165612        ← 父进程:fork 返回子进程的 pid
After: pid is 165612, fork return 0              ← 子进程:fork 返回 0

同一个 pid = fork() 调用,返回两次!

复制代码
父进程里 fork() 返回 165612(子进程 pid)→ 父拿到孩子的身份证号
子进程里 fork() 返回 0                    → 子知道自己"我是被复制出来的"

用返回值区分谁是谁:

复制代码
pid = fork();
if (pid == 0)      // 在子进程里
    ...子进程干活...
else if (pid > 0)  // 在父进程里
    ...父进程干活...

执行流示意图

复制代码
              父进程 (pid=165611)
                    │
              printf("Before...");     ← fork 之前,只跑一次
                    │
              pid = fork();            ← 分裂点
                    │
         ┌──────────┴──────────┐
         │                     │
    父进程继续              子进程开始
    pid = 165612            pid = 0
         │                     │
    printf("After...");    printf("After...");
         │                     │
      return 0;             return 0;
         │                     │
         ▼                     ▼

子进程不执行 fork 之前的代码,它从 fork 返回处开始跑。所以 fork 之前的 printf("Before") 子进程不会执行。


1-3 写时拷贝(COW)

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

**COW 是延迟申请技术------**因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证! 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率


1-4 fork 常规用法

用法一:父子各干各的

复制代码
父进程:while(1) { 等客户端连接 → fork → 让子进程去处理 }
子进程:处理这个连接的业务逻辑

用法二:变身(fork + exec)

复制代码
pid = fork();
if (pid == 0) {
    // 子进程
    exec("./mycmd");   // 子进程"变脸",换成另一个程序
}
// 父进程继续做自己的事

Shell 里敲 ls 就是这个流程:Bash fork 一个子进程,子进程 exec 变成 ls


1-5 fork 调用失败的原因

原因 说明
系统进程数太多 进程表满了,新 task_struct 分配失败
用户进程数超限 ulimit -u 限制了每个用户最大进程数

总结

概念 一句话
fork 做什么 复制出一个一模一样的新进程,从同一行代码分道扬镳
返回值 父子各收到不同的返回值,通过返回值知道"我是爹还是儿"
执行顺序 fork 之后谁先跑,调度器说了算
写时拷贝 共享到写时才复制,延迟申请,省内存
fork + exec fork 复制身份,exec 换脸

2. 进程终止


2-1 进程退出场景

三大类:

场景 举例
代码正常跑完,逻辑正确 mycmd 算完 1~100 累了,正常退休
代码正常跑完,逻辑错误 程序算完但结果不对,正常退休但留了封遗书
代码异常终止 段错误、除以 0、Ctrl+C 被人杀了

2-2 退出方法总览

复制代码
正常终止(有退出码可查 echo $?)
├── main 函数 return
├── exit(status)
└── _exit(status)

异常终止(无退出码)
├── Ctrl+C (SIGINT, 退出码 130)
├── kill (SIGTERM, 退出码 143)
└── 段错误 (SIGSEGV)、除以 0 (SIGFPE) 等

2-2-1 退出码

cpp 复制代码
cat > exit1.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("hello");    // 末尾没有 \n,输出留在缓冲区
    exit(0);            // exit 会先刷缓冲区再退出
}
EOF

cat > exit2.c << 'EOF'
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("hello");    // 末尾没有 \n,输出留在缓冲区
    _exit(0);           // _exit 不刷缓冲区,直接退出
}
EOF

刚才跑的结果:

复制代码
return 0 → echo $? 得到 0    ← 成功
return 1 → echo $? 得到 1    ← 失败
return 2 → echo $? 得到 2    ← 失败(另一种错误)

常见退出码速查:

退出码 含义
0 成功
1 一般错误(权限不足、除以 0 等)
2 参数错误
126 命令不可执行
127 命令未找到
128+n 被信号 n 干掉的(130=Ctrl+C, 143=kill)

0 = 成功,非 0 = 有事。


2-3-2 _exit 函数

复制代码
#include <unistd.h>
void _exit(int status);   // 只有低 8 位有效(0~255)

_exit 做的事:直接退出,不做任何清理。 缓冲区的内容?不管。atexit 注册的清理函数?不调。就是立刻走人。

复制代码
_exit(-1);    // -1 的二进制低 8 位是 11111111 = 255
echo $?        // 得到 255

2-3-3 exit 函数 vs _exit

cpp 复制代码
cd /root/workspace/linux_c++_redis/test && cat > exitdemo.c << 'EOF'
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main1()
{
    printf("hello");    // 末尾没有 \n,输出留在缓冲区
    exit(0);            // exit 会先刷缓冲区再退出
}

int main2()
{
    printf("hello");    // 同样没有 \n
    _exit(0);           // _exit 不刷缓冲区,直接退出
}
EOF

刚才跑出来的关键区别:

复制代码
exit(0) 版本:  输出 hello      ← printf 的缓冲区被刷了
_exit(0) 版本: 啥都没输出        ← 缓冲区里的 "hello" 直接丢了

原因:exit 在调用 _exit 之前多做了三步:

复制代码
exit(status)
    │
    ├── 1. 调用 atexit / on_exit 注册的清理函数  ← "临走收拾一下"
    ├── 2. 关闭所有打开的 FILE* 流,刷缓冲区        ← printf 没写出去的,现在写
    └── 3. 调用 _exit(status)                      ← 真正退出

_exit 直接跳到最后一步,前面的全不做。

复制代码
printf("hello");        printf("hello");
exit(0);                _exit(0);
    │                       │
    刷了 "hello"           缓冲区丢了
    输出 hello             (空白)

2-3-4 return 退出

复制代码
int main() {
    return 0;   // 等价于 exit(0);
}

return n 就是 exit(n) main 函数的调用者(C 运行时 _start)会拿返回值去调 exit,(调用main的运行时函数会 将main的返回值当做 exit的参数)

复制代码
_start → main() → return 0;
                  ↓
              exit(0) → _exit(0)

总结:exit / _exit / return 三者的关系

复制代码
return n
    │
    └── exit(n) ──→ atexit 清理 → 关闭流 → 刷缓冲区 → _exit(n) → 内核回收资源
                                                              ↑
                                           _exit(n) ──────────┘
                                        (直接退出,啥也不管)
_exit exit return
刷缓冲区
调 atexit 清理函数
关闭文件流
最终调 _exit

一句话:return n 就是 exit(n)exit 是带打扫的退出,_exit 是直接拔电源。

相关推荐
宵时待雨1 小时前
linux笔记归纳5:进程控制
linux·运维·笔记
代钦塔拉1 小时前
第一篇:工业级 C++/Qt 项目头文件包含原则:告别循环依赖与编译玄学
开发语言·c++·qt
夏日听雨眠2 小时前
LInux(gcc处理器,库文件,动静态库)
linux·运维·服务器
xingfujie2 小时前
Ubuntu K8s 1.28 kubeadm 高可用集群部署实战
linux·运维·服务器·docker·kubernetes
实心儿儿2 小时前
Linux —— 进程间通信 - 命名管道
linux·运维·服务器
funnycoffee1232 小时前
Cisco Firewpower 4100 9300 FXOS change management ip address
linux·数据库·tcp/ip
ZPC82102 小时前
规划后的轨迹,如何发给 moveit_servo 执行
c++·人工智能·算法·3d
杜子不疼.2 小时前
【C++ 在线五子棋对战】 - 工具类模块实现
开发语言·c++
青梅橘子皮2 小时前
Linux---开发工具(1)(vim,gcc/g++)
linux·运维·服务器