Linux进程控制详解02

Linux进程控制详解02

目录

  1. 进程创建
    1.1 fork函数初识
    1.2 fork函数返回值
    1.3 写时拷贝
    1.4 fork常规用法
    1.5 fork调用失败的原因

  2. 进程终止
    2.1 进程退出场景
    2.2 进程退出码
    2.3 进程正常退出
    2.3.1 return退出
    2.3.2 exit函数
    2.3.3 _exit函数
    2.3.4 return、exit和_exit之间的区别与联系
    2.4 进程异常退出

  3. 进程等待
    3.1 进程等待的必要性
    3.2 获取子进程status
    3.3 进程等待的方法
    3.3.1 wait方法
    3.3.2 waitpid方法
    3.3.3 多进程创建以及等待的代码模型
    3.3.4 基于非阻塞接口的轮询检测方案

  4. 进程程序替换
    4.1 替换原理
    4.2 替换函数
    4.3 函数解释
    4.4 命名理解
    4.5 做一个简易的shell


1. 进程创建

1.1 fork函数初识

在 Linux 系统中,fork 函数是进程创建的核心工具。它允许一个现有进程(父进程)创建一个新的进程(子进程)。这个新进程几乎完全复制了父进程的状态,包括代码段、数据段、堆栈以及打开的文件描述符等。

fork 的基本行为

当调用 fork 函数时,系统会创建一个新的进程。新进程(子进程)几乎完全复制了父进程的地址空间,包括代码、数据、堆栈等。子进程从 fork 函数的返回点开始执行,但它的返回值是 0,而父进程的返回值是新创建的子进程的 PID(进程标识符)。

#include 
#include 
#include 

int main() {
    pid_t pid = fork(); // 创建子进程

    if (pid < 0) {
        // 创建失败
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的PID是 %d\n", getpid());
    } else {
        // 父进程
        printf("父进程: 子进程的PID是 %d\n", pid);
    }

    return 0;
}

运行这段代码时,父进程和子进程都会继续执行 fork 之后的代码。这意味着,printf 语句会被执行两次:一次由父进程执行,另一次由子进程执行。

fork 的返回值

fork 函数的返回值是理解其行为的关键。父进程返回子进程的 PID,而子进程返回 0。这种设计使得父进程能够知道子进程的 PID,从而可以对其进行管理(如等待子进程结束),而子进程则知道自己是新创建的进程。

内核视角下的 fork

fork 被调用时,内核会执行以下操作:

  1. 分配新的内存块和内核数据结构:为子进程分配新的进程控制块(PCB)和地址空间。
  2. 复制父进程的数据结构:将父进程的部分数据结构(如文件描述符表、内存映射等)复制到子进程中。
  3. 将子进程添加到系统进程列表中:子进程被注册到操作系统中,成为可调度的进程。
  4. 返回值处理fork 返回两次,一次返回子进程的 PID 给父进程,一次返回 0 给子进程。
父子进程的执行顺序

fork 创建子进程后,父子进程的执行顺序由调度器决定。也就是说,父进程和子进程谁先执行是不确定的。

#include 
#include 
#include 

int main() {
    printf("Before fork\n"); // 父进程执行

    pid_t pid = fork(); // 创建子进程

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程执行\n");
    } else {
        // 父进程
        printf("父进程执行\n");
    }

    return 0;
}

在这个例子中,"Before fork" 只会被打印一次,因为这是在 fork 之前执行的。而 "子进程执行""父进程执行" 的顺序是不确定的,这取决于调度器的决策。

1.2 fork函数返回值

fork 函数的返回值机制是进程创建的核心之一。为什么 fork 会返回两次?为什么子进程返回 0,而父进程返回子进程的 PID?

为什么 fork 返回两次?

fork 函数的返回值机制源于进程创建的过程。当 fork 被调用时,父进程会执行一系列操作来创建子进程,包括分配内存、复制数据结构等。当子进程创建完成后,操作系统需要让父进程和子进程都能继续执行。因此,fork 函数在父进程和子进程中各返回一次,以表明它们各自的身份。

为什么子进程返回 0

子进程返回 0 的原因是,子进程不需要知道自己的 PID,因为它可以通过 getpid() 函数获取自己的 PID。而父进程需要知道子进程的 PID,以便对其进行管理。

为什么父进程返回子进程的 PID?

父进程返回子进程的 PID 是为了能够对子进程进行进一步的控制,比如等待子进程结束(通过 waitwaitpid)、发送信号等。一个父进程可能创建多个子进程,因此父进程必须能够区分这些子进程。

示例代码
#include 
#include 
#include 

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        printf("子进程: 我的PID是 %d\n", getpid());
    } else {
        printf("父进程: 子进程的PID是 %d\n", pid);
    }

    return 0;
}

在这个例子中,父进程和子进程都会打印各自的 PID,但子进程的 fork 返回值是 0,而父进程的返回值是子进程的 PID。

1.3 写时拷贝

fork 函数的一个重要特性是写时拷贝(Copy-on-Write)。这个机制优化了内存使用,提高了进程创建的效率。

写时拷贝的概念

写时拷贝是一种延迟复制技术。当 fork 创建子进程时,子进程和父进程共享相同的物理内存页,包括代码段、数据段和堆栈。只有当其中一个进程尝试修改共享内存时,操作系统才会复制相应的内存页,以确保两个进程的独立性。

为什么需要写时拷贝?

进程具有独立性,不能让子进程的修改影响父进程。然而,如果在创建子进程时立即复制所有内存页,会浪费大量资源。写时拷贝技术通过延迟复制,提高了资源利用率。

写时拷贝的实现
  1. 共享内存:子进程和父进程共享相同的物理内存页。
  2. 只读保护:共享的内存页被标记为只读。
  3. 触发复制:当任一进程尝试写入只读页时,会触发缺页异常,操作系统会复制该内存页,并将写权限授予触发写入的进程。
示例代码
#include 
#include 
#include 

int global_var = 100; // 全局变量

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        global_var = 200; // 修改全局变量
        printf("子进程: global_var = %d\n", global_var);
    } else {
        // 父进程
        sleep(1); // 等待子进程修改
        printf("父进程: global_var = %d\n", global_var);
    }

    return 0;
}

在这个例子中,子进程修改了全局变量 global_var,但父进程看到的仍然是原始值,这是因为写时拷贝机制的存在。

1.4 fork常规用法

fork 函数通常用于创建新进程,以便执行不同的任务。常见的用法包括:

1. 创建子进程处理客户端请求

服务器程序常常使用 fork 创建子进程来处理客户端请求。例如,当服务器接受一个客户端连接时,它会创建一个子进程来处理该连接,而父进程继续监听新的连接。

2. 执行新程序

子进程通常会调用 exec 系列函数来执行新的程序。例如,一个 shell 程序会使用 fork 创建子进程,然后调用 exec 执行用户输入的命令。

1.5 fork调用失败的原因

fork 函数调用失败可能有以下几种原因:

1. 系统资源不足

如果系统中已经有太多进程,或者可用内存不足,fork 调用可能会失败。

2. 进程数限制

每个用户在系统中可以创建的进程数是有限的。如果当前用户已经达到了这个限制,fork 调用也会失败。

示例代码
#include 
#include 
#include 

int main() {
    int i = 0;
    while (1) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            break;
        }
        if (pid == 0) {
            // 子进程直接退出
            return 0;
        }
        i++;
        printf("已创建 %d 个子进程\n", i);
    }
    return 0;
}

这个程序会不断创建子进程,直到系统资源耗尽或达到进程数限制为止。


2. 进程终止

2.1 进程退出场景

进程的退出场景主要有以下三种:

  1. 代码运行完毕,结果正确:例如,一个计算程序成功完成计算并返回结果。
  2. 代码运行完毕,结果不正确:例如,一个计算程序运行完毕,但计算结果错误。
  3. 代码异常终止:例如,进程因段错误、除零错误等原因崩溃。

2.2 进程退出码

进程退出时会返回一个退出码(exit code),用于指示程序的执行结果。退出码是一个整数,通常 0 表示成功,非零值表示错误。

退出码的意义

退出码可以帮助调用者判断程序的执行结果。例如,一个脚本可以通过检查退出码来决定下一步操作。

示例代码
#include 
#include 

int main() {
    int result = 0;

    // 模拟一个错误
    if (result != 100) {
        printf("程序执行失败\n");
        exit(1); // 退出码为1,表示错误
    }

    return 0;
}

在这个例子中,如果 result 不等于 100,程序会通过 exit(1) 终止,并返回退出码 1,表示执行失败。

2.3 进程正常退出

2.3.1 return退出

main 函数中使用 return 是最常见的进程退出方式。return 返回的值就是进程的退出码。

2.3.2 exit函数

exit 函数可以在程序的任何位置调用,它会终止进程,并执行一些清理操作,如关闭文件、释放内存等。

2.3.3 _exit函数

_exit 函数与 exit 类似,但它不会执行清理操作,而是直接终止进程。

2.3.4 return、exit和_exit之间的区别与联系
函数 行为 适用场景
return main 函数返回 算法结束
exit 终止进程并执行清理操作 正常退出
_exit 直接终止进程,不执行清理操作 异常退出

2.4 进程异常退出

进程异常退出通常是因为程序发生了严重错误,如段错误、除零错误等。

示例代码
#include 
#include 

int main() {
    int a = 10;
    int b = 0;
    int c = a / b; // 除零错误
    return 0;
}

这个程序会因为除零错误而崩溃,操作系统会发送 SIGFPE 信号给进程,导致其异常终止。


3. 进程等待

3.1 进程等待的必要性

当父进程创建子进程后,子进程的生命周期可能比父进程短,也可能长。父进程通常需要等待子进程结束,以获取其退出状态。

为什么需要等待?
  1. 获取子进程的退出状态:父进程可以通过 waitwaitpid 获取子进程的退出码,从而判断子进程的执行结果。
  2. 避免僵尸进程:如果父进程不等待子进程结束,子进程会变成僵尸进程,占用系统资源。

3.2 获取子进程status

子进程的退出状态可以通过 waitwaitpid 函数获取。这两个函数的原型如下:

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

status 参数用于存储子进程的退出状态。

3.3 进程等待的方法

3.3.1 wait方法

wait 函数会阻塞父进程,直到任意一个子进程结束。

#include 
#include 
#include 
#include 

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程执行完毕\n");
        exit(0);
    } else {
        // 父进程等待子进程
        int status;
        wait(&status); // 阻塞等待子进程结束
        printf("子进程退出码: %d\n", WEXITSTATUS(status));
    }

    return 0;
}

在这个例子中,父进程调用 wait 阻塞等待子进程结束,并通过 WEXITSTATUS 获取子进程的退出码。

3.3.2 waitpid方法

waitpid 函数提供了更灵活的等待机制,可以指定等待特定的子进程。

#include 
#include 
#include 
#include 

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程执行完毕\n");
        exit(0);
    } else {
        // 父进程等待子进程
        int status;
        pid_t result = waitpid(pid, &status, 0); // 等待特定子进程
        if (result == pid) {
            printf("子进程退出码: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

在这个例子中,父进程调用 waitpid 明确等待特定的子进程结束。

3.3.3 多进程创建以及等待的代码模型
#include 
#include 
#include 
#include 

int main() {
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return 1;
        } else if (pid == 0) {
            // 子进程
            printf("子进程 %d 执行完毕\n", getpid());
            exit(i); // 返回子进程编号作为退出码
        }
    }

    // 父进程等待所有子进程
    int status;
    for (int i = 0; i < 3; i++) {
        pid_t pid = wait(&status);
        printf("子进程 %d 退出码: %d\n", pid, WEXITSTATUS(status));
    }

    return 0;
}

这个程序创建了三个子进程,并通过 wait 等待所有子进程结束。

3.3.4 基于非阻塞接口的轮询检测方案

waitpid 函数可以通过设置 WNOHANG 标志实现非阻塞等待。

#include 
#include 
#include 
#include 

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        sleep(3); // 模拟长时间运行
        printf("子进程执行完毕\n");
        exit(0);
    } else {
        // 父进程非阻塞等待
        int status;
        while (1) {
            pid_t result = waitpid(pid, &status, WNOHANG);
            if (result == 0) {
                printf("子进程尚未结束,继续轮询\n");
                sleep(1); // 等待1秒
            } else if (result == pid) {
                printf("子进程退出码: %d\n", WEXITSTATUS(status));
                break;
            } else {
                perror("waitpid");
                break;
            }
        }
    }

    return 0;
}

在这个例子中,父进程使用 waitpidWNOHANG 标志实现非阻塞等待,避免了长时间的阻塞。


4. 进程程序替换

4.1 替换原理

进程程序替换是指用新程序替换当前进程的地址空间。替换后,原来的进程代码和数据会被新程序的代码和数据替换,但进程的 PID 保持不变。

4.2 替换函数

exec 系列函数用于进程程序替换。常见的函数包括:

  • execl
  • execv
  • execle
  • execve
  • execlp
  • execvp

4.3 函数解释

execl
int execl(const char *path, const char *arg, ...);

execl 用于执行新程序,参数以 NULL 结尾。

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

execv 的参数是一个指针数组,数组以 NULL 结束。

execle
int execle(const char *path, const char *arg, ..., char *const envp[]);

execle 允许传递环境变量。

execve
int execve(const char *filename, char *const argv[], char *const envp[]);

execveexec 系列函数的底层实现。

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

execlp 会在 PATH 环境变量中查找可执行文件。

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

execvpexecv 的变体,支持 PATH 查找。

4.4 命名理解

exec 系列函数的命名规则如下:

  • l:参数以列表形式传递。
  • v:参数以指针数组形式传递。
  • p:支持 PATH 查找。
  • e:允许传递环境变量。

4.5 做一个简易的shell

4.5.1 shell 的工作原理

一个 shell 程序的基本工作流程如下:

  1. 获取命令行:读取用户输入的命令。
  2. 解析命令行:将命令分解为程序路径和参数。
  3. 创建子进程:通过 fork 创建子进程。
  4. 替换子进程:使用 exec 系列函数执行用户输入的命令。
  5. 等待子进程退出:父进程通过 waitwaitpid 等待子进程结束。
4.5.2 示例代码
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_CMD 1024
#define MAX_ARGS 64

int main() {
    char cmd[MAX_CMD]; // 存储用户输入的命令
    while (1) {
        // 1. 获取命令行
        printf("myshell> ");
        fflush(stdout);
        if (!fgets(cmd, MAX_CMD, stdin)) {
            printf("\n");
            break;
        }
        cmd[strlen(cmd) - 1] = '\0'; // 去掉换行符

        // 2. 解析命令
        char *args[MAX_ARGS];
        int i = 0;
        args[i++] = strtok(cmd, " ");
        while ((args[i++] = strtok(NULL, " ")) != NULL);

        // 3. 创建子进程
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            continue;
        } else if (pid == 0) {
            // 子进程执行命令
            execvp(args[0], args);
            perror("execvp"); // 如果execvp返回,说明执行失败
            exit(1);
        } else {
            // 父进程等待子进程
            int status;
            waitpid(pid, &status, 0);
            printf("子进程退出码: %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

这个简单的 shell 程序实现了命令行解析、子进程创建、命令执行和等待子进程结束的功能。


你可能感兴趣的:(LINUX,linux,服务器,运维,算法,链表,贪心算法)