关键词搜索

源码搜索 ×
×

C 语言编程 — fork 进程操作

发布2023-04-24浏览741次

详情内容

目录

用户进程

User Process 由 Kernel 创建、调度和销毁,运行在 User Space 中,是系统资源分配的单元。

在 Kernel 中 User Process 使用一一对应的 task_struct 结构体表示。task_struct 是一个非常庞大的数据结构,存储了 User Process 的所有信息,Kernel 以此来对 User Process 进行管理,所以也称为 PCB(Process Control Block,进程控制块),或进程描述符(Process Descriptor)。

// include/linux/sched.h

struct task_struct {
    volatile long state;               // 进程状态
    void   *stack;                     // 进程堆栈指针
    struct list_head tasks;            // 进程链表指针
    pid_t  pid;                        // 进程 ID
    pid_t  tgid;                       // 进程组 ID
    struct task_struct *parent;        // 父进程指针
    struct list_head children;         // 子进程链表指针
    struct mm_struct *mm;              // 进程地址空间指针
    struct files_struct *files;        // 进程文件描述符表指针
    struct signal_struct *signal;      // 进程信号表指针
    struct sighand_struct *sighand;    // 进程信号处理函数表指针
    struct task_struct *group_leader;  // 进程组领导指针
    struct completion *vfork_done;     // vfork 子进程完成事件
    int vfork_mode;                    // vfork 子进程标志

    ...
};

    进程调度的状态机

    • 创建状态(New):进程正在被创建中,尚未到就绪状态。
    • 就绪状态(Ready):进程已处于准备运行状态,即:进程获得了除了 CPU 之外所需的一切资源,一旦得到 CPU,即可运行。
    • 运行状态(Running):进程正在 CPU 上运行。
    • 阻塞状态(Waiting):进程正在等待某一事件而暂停运行,此时不会占用 CPU,例如:I/O 场景中,在等待 Buffer 就绪。
    • 结束状态(terminated):释放进程资源。

    在这里插入图片描述

    在这里插入图片描述

    子进程

    在这里插入图片描述

    1. Parent 调用 fork() 指示创建 Child:Kernel 首先会新建 Child PCB,设置 PID,并关联 Parent PCB。
    #include <unistd.h>
    
    pid_t fork(void);
    
    • 1
    • 2
    • 3
    1. fork() 会特殊的返回 2 次:程序需要通过 if/else 语句来判断 2 个不同的 pid 数值,以此区分 Parent 和 Child 的处理逻辑。

      • pid > 0:是返回给 Parent 的 Child PID。
      • pid = 0:表示当前代码块运行在 Child。
      • pid < 0:创建失败,可以通过 errno 变量获取错误码。失败的原因可能是系统资源不足,或者是 Parent 已经创建了太多的 Childs。
    2. Parent 调用了 exec() 后,Child 才正式启动:Kernel 为 Child 分配 VAS(Virtual Address Space,虚拟地址空间),并将 Parent VAS 的数据 Copy 到 Child VAS,然后初始化 Child VAS 中的 PC(Process Count,程序计数器)值,开始 CPU 执行。可见,Parent 和 Child 的唯一区别就是 PID 不同而已。

    NOTE:exec() 实现了一种 COW(Copy On Write,写时复制)机制,即:只有在需要的时候才开始 Copy 数据,避免不必要的开销,因为 Parent VAS 中的数据量可能非常庞大。

    1. Child 完成 exec() 后,跟着调用 exit() 指示开始退出:Child 需要向 Parent 返回执行状态。此时的 Child 首先会进入僵尸进程状态,然后等待 Parent 切实接收到返回。

    2. Parent 调用 wait() 等待 Child 退出,并获取 Child 的执行状态:切实收到 Child 的返回后,才开始释放 Child 的所有资源。以此来避免了 Child 执行状态的丢失。

    示例代码:通过 pid 数值判断,在 if/else 代码块中实现父子进程各自的逻辑。

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main() {
        pid_t pid;
        int status;
    
        pid = fork();
        if (pid == 0) {
            // 子进程中执行 ls 命令
            execlp("ls", "ls", "-l", NULL);
            printf("exec failed\n");
        } else if (pid > 0) {
            // 父进程中等待子进程结束
            wait(&status);
            printf("child process exited with status %d\n", status);
        } else {
            printf("fork failed\n");
        }
    
        return 0;
    }
    
      21
    • 22
    • 23

    Child exec 函数族

    这些函数都会在当前的 Parent 中执行一个新的 Child 程序。

    • execl 函数
      • path 参数:可执行程序的文件名(绝对路径);
      • arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
    int execl(const char *path, const char *arg, ...);
    
    • 1
    • execle 函数
      • path 参数:可执行程序的文件名(绝对路径);
      • arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
      • envp 参数:环境变量数组,最后一个元素必须是 NULL。
    int execle(const char *path, const char *arg, ..., char *const envp[]);
    
    • 1
    • execlp 函数
      • file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
      • arg 可变长参数:传递给可执行程序的参数,最后一个参数必须是 NULL。
    int execlp(const char *file, const char *arg, ...);
    
    • 1
    • execv 函数
      • path 参数:可执行程序的文件名(绝对路径);
      • argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
    int execv(const char *path, char *const argv[]);
    
    • 1
    • execvp 函数
      • file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
      • argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
    int execvp(const char *file, char *const argv[]);
    
    • 1
    • execvpe 函数
      • file 参数:可执行程序的文件名(仅文件名),它会在 PATH 系统环境变量路径中搜索。
      • argv 参数:传递给可执行程序的参数,指针数组类型,最后一个元素必须是 NULL。
      • envp 参数:环境变量数组,最后一个元素必须是 NULL。
    int execvpe(const char *file, char *const argv[], char *const envp[]);
    
    • 1

    Parent wait 函数族

    这些函数都是阻塞调用,即 Parent 会一直等待直到 Child 结束才会返回。

    • wait 函数:等待一个 Child 退出。
      • status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
      • 函数返回值
        • 成功:返回退出的 Child PID。
        • 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
    pid_t wait(int *status);
    
    • 1
    • waitpid 函数:等待一个指定 PID 的 Child 退出。
      • pid 参数:指定 Child。如果 pid 为 -1,则等待任意一个 Child 退出。
      • status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
      • options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
      • 函数返回值
        • 成功:返回退出的 Child PID。
        • 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
    pid_t waitpid(pid_t pid, int *status, int options);
    
    • 1
    • wait3 函数:等待一个 Child 退出,同时返回 Child 的资源使用情况,如 CPU 占用时间、内存使用情况等。
      • status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
      • options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
      • rusage 参数:存储 Child 的资源使用情况。
      • 函数返回值
        • 成功:返回退出的 Child PID。
        • 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
    pid_t wait3(int *status, int options, struct rusage *rusage);
    
    • 1
    • wait4 函数:等待一个指定 PID 的 Child 退出,同时返回 Child 的资源使用情况,如 CPU 占用时间、内存使用情况等。
      • pid 参数:指定 Child。如果 pid 为 -1,则等待任意一个 Child 退出。
      • status 参数:指针类型,如果不为 NULL,则存储 Child 退出状态。
      • options 参数:指定等待选项,例如:WNOHANG、WUNTRACED 等。
      • rusage 参数:存储 Child 的资源使用情况。
      • 函数返回值
        • 成功:返回退出的 Child PID。
        • 失败:返回 -1。错误码包括 ECHILD(没有子进程)、EINTR(等待被信号中断)、EINVAL(pid 参数非法)等。
    pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
    
    • 1

    如果关心 Child 的退出状态,可以使用 WIFEXITED()、WEXITSTATUS()、WIFSIGNALED()、WTERMSIG()、WIFSTOPPED() 和 WSTOPSIG() 宏来解析 status 参数。

    多进程

    IPC(多进程间通信)

    Linux 操作系统中提供了多种不同的 IPC(Inter-Process Communication,进程间通讯)方式,来支持 Multi-Processes 之间的数据共享和通信。

    • 匿名管道(pipe):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。

    • 命名管道(named pipe): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道。命名管道以普通文件的形式存在,严格遵循 FIFO,可以实现本机任意两个进程间通信。

    • 共享内存(shared memory):是一种高效的进程间通信方式。使得多个进程可以访问同一块物理内存空间,多个进程间可以互相看见对方对共享数据的更新。Linux 提供了多种共享内存方式,例如:mmap 共享内存、XSI 共享内存、POSIX 共享内存等。这种方式需要依赖同步原语,如:互斥锁、信号量等。

    • 套接字(UNIX Socket):一个进程作为服务器监听 UNIX Socket,并接收客户端的请求;另外的进程作为客户端连接到 UNIX Socket,并向服务器发送请求。

    • 消息队列(Message Queuing):消息队列是一个存放在内存中的链表结构,由 Kernel 管理,具有特定的格式。

    • 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

    pipe() 匿名管道

    C 语言的匿名管道 pipe() 定义在 unistd.h(Unix standard)中。

    函数作用:创建一个管道,本质是一个 Kernel Byte Steam Buffer(字节流缓冲区),大小为 4KB,支持 FIFO 队列,数据写入管道的一端,可以从另一端读取出来。

    函数原型

    • pipefd[2] 参数:是一个长度为 2 的整数数组,它包含了两个 fd(文件描述符),一个用于读取数据,另一个用于写入数据。
      • pipefd[0]:读端口,从队头(Front)读。
      • pipefd[1]:写管道,从犯队尾(Rear)写。
    • 函数返回值
      • 成功:0,并会代表管道两端的 2 个 fd 存储在数组中。
      • 失败:-1,并设置了 errno。
    #include <unistd.h>
    
    int pipe(int pipefd[2]);
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    示例程序:程序创建一个子进程,并创建了一个管道,子进程向管道写入一条消息,父进程从管道读取这条消息并输出到终端上。

    由于子进程和父进程拥有完全相同的变量,因此子进程也有对应 pipefd[2](管道读端和写端)的两个 fd。之后,只需要关闭一侧的读端和另一侧的写端,就可以实现进程间的通信。

    在这里插入图片描述

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define BUF_SIZE 256
    
    int main() {
        int pipefd[2];
        char buf[BUF_SIZE];
        pid_t pid;
    
        if (pipe(pipefd) == -1) {
            perror("pipe");
            exit(EXIT_FAILURE);
        }
    
        pid = fork();
        if (pid == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        }
    
        if (pid == 0) { // child process
            close(pipefd[0]); // close the read end of the pipe
            strcpy(buf, "Hello, parent process!\n");
            write(pipefd[1], buf, strlen(buf));
            close(pipefd[1]); // close the write end of the pipe
            exit(EXIT_SUCCESS);
        } else { // parent process
            close(pipefd[1]); // close the write end of the pipe
            while (read(pipefd[0], buf, BUF_SIZE) > 0) {
                printf("Received message: %s", buf);
            }
            close(pipefd[0]); // close the read end of the pipe
            exit(EXIT_SUCCESS);
        }
    }
    
      21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    dup2() 管道重定向

    dup2() 函数,可以用于将一个文件描述符复制到另一个文件描述符上。在管道场景中,可通过 dup2 修改 fds,继而用于实现管道读写端的重定向。

    函数原型

    • oldfd 参数:是需要被复制的 fd;
    • newfd 参数:是重定向的目标 fd。如果 newfd 已经被打开了,那么 dup2() 会先关闭它,然后将 oldfd 复制到 newfd 上。
    #include <unistd.h>
    
    int dup2(int oldfd, int newfd);
    
    • 1
    • 2
    • 3

    程序示例:将 STDOUT_FILENO(标准输出文件描述符)重定向到一个文件中。首先 open() 一个文件,并指定了写入权限。然后,使用 dup2() 函数将 fd 复制到 STDOUT_FILENO 上。这样,所有输出 STDOUT_FILENO 的数据都会最终重定向到 output.txt 文件中。

    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    
    int main() {
        int fd;
    
        fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
        if (fd == -1) {
            perror("open");
            return -1;
        }
    
        if (dup2(fd, STDOUT_FILENO) == -1) {
            perror("dup2");
            return -1;
        }
    
        printf("Hello, world!\n"); // this will be written to "output.txt"
    
        close(fd);
        return 0;
    }
    
      21
    • 22
    • 23

    命名管道

    匿名函数使用简单,但问题是只能用于父子进程之间,因为在父进程创建的 pipe,可以通过 fork() 的方式复制到子进程,然后两者使用同一个 pipe 进行通信。

    而对于非相关进程而言,则需要使用命名管道。为了保证数据的安全,同样采用了阻塞的 FIFO,让写操作变成原子操作,行为与匿名管道类似。

    函数原型:命名管道,在 Linux 文件系统中以文件的形式存在,由 filename 指定名称,而 mode 则指定了文件的读写权限。

    #include <sys/types.h>
    #include <sys/stat.h>
    
    int mkfifo(const char *filename, mode_t mode);
    int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    由于是一个真实的文件,所以命名管道也可以使用 CLI 来进行创建:

    $ mkfifo fifo_file
    $ mknod fifo_file p
    
    • 1
    • 2

    相关技术文章

    点击QQ咨询
    开通会员
    返回顶部
    ×
    微信扫码支付
    微信扫码支付
    确定支付下载
    请使用微信描二维码支付
    ×

    提示信息

    ×

    选择支付方式

    • 微信支付
    • 支付宝付款
    确定支付下载