cen's blog cen's blog
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • 《JavaScript教程》
    • C++学习
    • C++数据结构
    • MySQL
    • Linux
  • 高中时代
  • 工作日常
  • CLion
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

cen

十年饮冰,难凉热血
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • 《JavaScript教程》
    • C++学习
    • C++数据结构
    • MySQL
    • Linux
  • 高中时代
  • 工作日常
  • CLion
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • Linux环境搭建
  • 基本指令
  • 权限
  • Linux基础开发工具
  • 进程概念
  • 进程控制
    • 进程创建
      • fork函数
      • 返回值
      • 用法
    • 进程终止
      • 退出码
    • 进程等待
      • 进程等待的函数
    • 阻塞 vs 非阻塞
    • 进程程序替换
      • 替换函数
      • 替换原理
  • 基础IO流
  • 动态库和静态库
  • 进程通信
  • 进程信号
  • 多线程
  • 线程安全
  • Linux
cen
2025-02-15
目录

进程控制

# 进程创建

# fork函数

**fork函数创建子进程:**fork从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

内核做的工作:

  1. 进程调用fork,当控制转移到内核中的fork代码后,内核做:
  2. 分配新的内存块和内核数据结构给子进程
  3. 将父进程部分数据结构内容拷贝至子进程
  4. 添加子进程到系统进程列表当中
  5. fork返回,开始调度器调度

执行下面的代码:

#include<iostream>

using namespace std;
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int global_val = 100;

int main() {
    // 物理空间 or 虚拟空间
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        global_val = 200;
        cout << "子进程:" << "PID = " << getpid() << " PPID = " << getppid() << " global_val = " << global_val << endl;
    } else if (id > 0) {
        sleep(3);
        cout << "父进程:" << "PID = " << getpid() << " PPID = " << getppid() << " global_val = " << global_val << endl;;
    } else {
        cout << "fork error" << endl;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出结果:

子进程:PID = 3111169 PPID = 3111168 global_val = 200
父进程:PID = 3111168 PPID = 3096584 global_val = 100
1
2

会发现父进程在打印全局变量的时候,并没有被子进程修改,为什么呢?

写时拷贝 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,这样可以高效的使用内存空间

# 返回值

  • 如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0
  • 如果子进程创建失败,则在父进程中返回 -1

# 用法

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

# 进程终止

进程终止有三种情况:代码运行完毕,结果正确(return 0)、代码运行完毕,结果不正确(return !0)和代码异常终止。

# 退出码

以0表示代码执行成功,以非0表示代码执行错误

  1. main函数 return 退出码 = exit(退出码)
  2. exit(退出码):exit函数可以在代码中的任何地方退出进程,并且会自动刷新缓冲区,这点与_exit不同(这里的缓冲区在用户级的缓冲区)

# 进程等待

子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。所以,父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。

# 进程等待的函数

#include <sys/wait.h>
#include <sys/types.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
1
2
3
4
5

wait函数表示等待任意子进程,参数表示子进程的状态,不关心可以设置为NULL,如果等待成功返回子进程的PID,否则返回-1

waitpid函数表示等待指定或者任意子进程

  • 参数:
  1. pid:待等待子进程的pid,若设置为-1,则等待任意子进程,等价于wait
  2. status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
  3. options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
  • 返回值
  1. 等待成功返回被等待进程的pid。
  2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

status:

status是一个输出型参数,由操作系统进行填充。如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

exitCode = (status >> 8) & 0xFF; // 退出码
exitSignal = status & 0x7F;      // 退出信号
1
2

# 阻塞 vs 非阻塞

当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

#include<iostream>
using namespace std;
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t id = fork();
    if (id == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            cout << "子进程:" << getpid() << "父进程:" << getppid() << endl;
            cnt--;
            sleep(1);
        }
        exit(12);
    } else if (id > 0) {
        // parent: wait child
        int status = 0;
        // int ret = waitpid(id, &status,WNOHANG);              // 非阻塞等待
        // cout << "exitcode: " << ((status >> 8) & 0xFF) << " " << "exitsig: " << (status & 0x7F) << endl;
        while (true) {   // 轮询
            int ret = waitpid(id, &status, WNOHANG);         // 非阻塞等待
            if (ret == 0) {
                cout << "pid指定的子进程没有退出!" <<
                     endl;
            } else if (ret > 0) {
                // 返回子进程的pid了 ret == id
                cout << "等待成功,子进程退出!" << endl;
                cout << "exitcode: " << ((status >> 8) & 0xFF) << " " << "exitsig: " << (status & 0x7F) << endl;
                cout << "ret: " << ret << endl;
                break;
            } else {
                // waitpid调用失败
                cout << "waitpid call fail!" << endl;
                break;
            }
            sleep(1);
        }
    } else {
        cout << "fork error!" << endl;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 进程程序替换

fork创建的子进程可以执行和父进程相同的程序,也可以执行一段全新的程序。这个过程就是进程程序替换。

# 替换函数

使用exec函数:

#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, .../*, (char *) NULL */);
int execlp(const char *file, const char *arg, .../*, (char *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
1
2
3
4
5
6
7
8
9
10

int execl(const char *pathname, const char *arg, .../*, (char *) NULL */);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

int execlp(const char *file, const char *arg, .../*, (char *) NULL */);

第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾

int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。

int execv(const char *pathname, char *const argv[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

int execvp(const char *file, char *const argv[]);

第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾

int execvpe(const char *file, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。

例如:

execl("/usr/bin/ls","ls","-a","-l",NULL);     // 执行ls
1

# 替换原理

用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。

上次更新: 2025/03/14, 11:20:46
进程概念
基础IO流

← 进程概念 基础IO流→

最近更新
01
线程安全
05-21
02
cmake教程
05-08
03
项目
05-07
更多文章>
Theme by Vdoing | Copyright © 2024-2025 京ICP备2020044002号-3 京公网安备11010502056119号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式