Linux 进程编程

本文详细介绍了Linux下的进程编程,包括预备知识、fork()函数创建新进程、进程的终止方法如_exit()和exit()、wait()函数等待子进程、execve()加载新程序,以及进程生命的整体理解。通过示例代码展示了进程的创建、执行和退出过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

预备知识

首先你要清楚,进程管理为内核的功用之一。

接着我们遵循教科书中的定义:进程是程序执行时的一个实例。

再进一步解释,程序(program)为存储在硬盘上的二进制文件,而将其装载进内存之后即为进程(process),同时还将获得一个 PID(process ID)用于唯一标识该进程。

进程被产生,他们有或多或少的生命,他们也可以继续产生一个或多个子进程,但最终都要死亡。与人类不同的是,进程没有性别,每个进程都只有一个父进程。

创建新进程 fork()

fork() 通过复制调用进程的方式创建一个新的进程,新进程被称为子进程,调用进程则被称为父进程。

函数原型如下:

pid_t fork(void);

创建一个进程时,它几乎与父进程相同。就像 fork() 帮助手册上所写,子进程就是对父进程的精确复制。

The child process is an exact duplicate of the parent process

也就是说,子进程将复制父进程所拥有的资源,即拷贝父进程的整个地址空间。之后这两个进程将执行相同的程序文本,但彼此将拥有独立的栈、堆及数据。

//  代码示例
int main(int argc, char *argv[])
{
    switch (  fork() ) {
        case -1: // Handle error
            perror("fork error");
            exit(1);

        case 0:  // Perform actions specific to child
            printf("This is child process, PID is %d\n", getpid());
            printf("My PPID is %d\n", getppid());
            break;

        default: // Perform actions specific to parent
            printf("This is parent process, PID is %d\n", getpid());
    }   

    printf("This will be shown in both parent and child! %d\n\n", getpid());
                                                                        
    return 0;
}

当系统调用 fork() 成功创建子进程后,将在父进程中返回子进程的 PID,并在子进程中返回 0。

因此我们才需要通过 fork() 的返回值来区分父、子进程,但 fork() 之后你不应对父、子进程的执行顺序做任何假设,这时你就需要后文的 wait() 函数。

此处需要理解的关键是,完成一次 fork() 调用后,将存在两个进程,并且每个进程都将从 fork() 的返回处继续执行。
forkLearn 执行结果
代码执行结果如上所示,先抛开父、子进程执行顺序的问题,运行结果确是如我们预期。

细心观察的你会发现,我们无意间产生了一个 PPID(Parent PID) 为 1 的进程,这就是所谓的孤儿进程。因其父进程先终止,而变为孤儿进程,接着孤儿进程便会被 PID 为 1 的 systemd 进程接管。

进程的终止 _exit() 与 exit()

这两种退出进程的方式,都是进程的正常退出。不同的是 _exit() 函数是更为底层的系统调用,而 exit() 函数则是 stdlib.h(standard library)C 标准库中的函数。

程序一般不会直接调用 _exit() 而是调用 exit(),因其经过封装后会在执行 _exit() 前进行一些清理操作。

void exit(int status);

exit() 函数没有返回值,整型参数 status 接收进程退出时的状态码。

C 程序 main() 函数里的return 0等同于执行exit(0)的调用,因为 main 的返回值将作为 exit() 的参数来退出程序。

等待子进程 wait()

系统调用 wait() 用于推迟调用进程的执行(即父进程挂起自己),直至它任一子进程的终止。

pid_t wait(int *wstatus);

执行成功将返回被终止的子进程 PID,错误返回 -1。而至于他的整型 wstatus 参数,我们这里直接给它传入 NULL 即可。

int main(int argc, char *argv[])
 {
    int j;

    if ( argc < 2 || strcmp(argv[1], "--help") == 0)
    {   
        printf("try again! and type at lease one argument.\n");
        exit(1);
    }

    for (j = 1; j < argc; j++) // Create one child for each argument
    {   
        switch (fork()) {
            case -1: 
                perror("fork()");
                exit(1);

            case 0: // 因子进程的 fork() 返回 0 而进入此条件
                printf("the PID of child %d is: %d\n", j, getpid());
                printf("My parent is %d\n", getppid());
                sleep(5);
                printf("I (child) wake up!\n\n");
                exit(0);

            default: // 父进程成功生成子进程后,因父进程的 fork() 返回子进程 PID 大于零,因此父进程进入 default,从>而执行以下语句,wait 着子进程的释放,以开始新一轮的循环
                printf("I am the parent, my PID: %d\n", getpid());
                // 上面 printf 语句与 wait() 的顺序决定了,父进程是先等待还是先输出 PID 内容
                     
                printf("the return value of wait is: %d\n\n", wait(NULL));
         // 若在此处注释 wait() 语句,则子进程的 PPID 将显示为 1,即成为孤儿进程,被 init 接管
        }
    }

    // Now, just in the parent.
    printf("I (parent) finished my jobs.\n");
        
    return 0;
}

上面的程序将根据命令行中传入的参数个数,来创建同等数量的子进程。父进程在打印提示信息后,将 wait() 着子进程的 sleep(),你将根据输出顺序来搞清楚,父、子进程的创建、阻塞逻辑关系。
在这里插入图片描述
wait() 的另一直观感受——我们终端上的 shell 正是在这样做:只有当执行命令的子进程退出后,shell 才会打印出命令提示符。

加载新程序 execve()

系统调用 execve(),丢弃旧程序,后加载一个新程序到当前进程内存,即进程的栈、堆及数据都将被替换。

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

filename 指向即将被加载到当前进程空间的程序,可以是二进制可执行文件,也可以是脚本文件。

argv[] 是将传递给新程序的数组型的参数,在需要以 NULL 结尾的同时,argv[0] 的值应与将执行的文件名相同。

envp 则指定了新程序的环境列表。execve() 在成功时不返回任何值,仅在失败时返回 -1。

库函数 exec()

接下来的 exec() 函数族便是建构在 execve() 之上,为 execve() 的执行提供了多种选择方式。

这里我们先以 execvp() 为例:

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

execvp() 接受以数组形式的参数传递(需要以 NULL 结尾),同时允许用户只提供程序名,系统将会在环境变量 PATH 所指定的目录下来寻找相应的可执行文件。

// execvp_pro.c
int main(int argc, char *argv[])
{
    pid_t childPid;
    pid_t waitReturnNum;

    char *arg_list[] = {
        "ls",
        "-lh",
        "--color=tty",
        "./",
        NULL
    };

    switch ( childPid = fork() ) {
        case -1:
            perror("fork");
            exit(1);

        case 0:   // child, do execvp job.
            printf("the PID of the child: %d\n", getpid());
            printf("My parent is %d\n", getppid());

            execvp("ls", arg_list);  // 数组的形式接收参数,并在 PATH 路径下寻找 ls 程序文件

            // if run the next line.
            printf("*********************************** Just a test line.\n");
            // NO
            // The exec() family of functions replaces the current process image with a new process image.

        default:  // paretn, do wait job.
            waitReturnNum =  wait(NULL);
            printf("\nParent after wait, the waitReturnNum:%d\n", waitReturnNum); 
            printf("the childPid: %d\n", childPid);
    }
    return 0;
}

接下来对比 execv() 函数,以下代码仅仅列出修改后的部分

// execv_pro.c
char *arg_list[] = { 
        NULL
    }; 

execv("hello", arg_list);   // hello 为当前目录下已有的可执行程序文件

接着我们根据以上文件名,顺序执行如下编译命令:

gcc  execvp_pro.c  -g  -o  hello;  
gcc  execv_pro.c   -g  -o  exec
# 此时的 hello 程序,即为 execv_pro.c 文件中 execv() 将要接收的参数

执行结果如下所示:
在这里插入图片描述
第一次 ./hello的执行,是在子进程中装载 /bin/ls程序(在 PATH 中寻找所得。)接下来 ./exec的执行,是在当前目录下寻找到的 hello程序的装载。

以上所示,便为两个函数的 const char *fileconst char *path参数的区别。

总体来看进程的生命

父进程(程序 A)的执行,或将引发 fork() 操作。

子进程完成对父进程的复制后的 execve() 为可选操作,因为有时我们希望子进程执行与父进程相同的操作,有时我们却希望子进程能丢弃现有程序并加载一个新的程序。

同样父进程的 wait() 操作也为可选,我们可以挂起父进程等待着子进程的终止,当然也可以让父进程对子进程不管不顾。
图片来源http://libgen.is/book/index.php?md5=4B83F6C2F3F5C73FF81391A98679E8CE

图片来源:《UNIX系统编程手册 上》.((德)Michael Kerrisk ).[PDF]

到最后了,还记得刚刚那个接管孤儿进程的那个 1 号进程吗?

这是 Linux 系统启动后所创建的 第一个用户态进程,因此根据前面的知识我们便会知道,后续的所有进程都称得上的 1 号进程的子孙。

如果你想验证的话,不妨执行 pstree -p看看吧。

Reference

  • 《深入理解 Linux 内核》
  • 《UNIX系统编程手册 上》.((德)Michael Kerrisk ).[PDF] :http://libgen.is/book/index.php?md5=4B83F6C2F3F5C73FF81391A98679E8CE
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值