从入门到精通:Linux进程控制

在计算机操作系统中,进程(Process)是一个非常重要的概念。进程控制是操作系统的核心功能之一,对于Linux操作系统尤其如此。本文将详细介绍Linux操作系统中的进程控制,从入门到精通,涵盖进程的创建、终止、等待以及程序替换等内容。

进程创建
fork函数初识

在Linux中,fork函数是创建新进程的最重要函数之一。通过fork函数,一个已存在的进程可以创建一个新进程。新进程称为子进程,而原进程称为父进程。这个过程在操作系统的进程管理中起着至关重要的作用。

当一个进程调用fork函数时,操作系统会进行一系列复杂的操作,包括分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表中,最终返回到用户空间,开始调度器调度。

fork函数的关键在于它在父进程和子进程中分别返回不同的值。在父进程中,fork返回子进程的PID,而在子进程中,fork返回0。如果fork调用失败,则返回-1。这使得父进程和子进程可以根据返回值来执行不同的代码,从而实现并发执行。

#include <unistd.h>
#include <stdio.h>

int main(void) {
    pid_t pid;

    printf("Before: pid is %d\n", getpid());

    pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    }

    printf("After: pid is %d, fork returned %d\n", getpid(), pid);
    return 0;
}

在这个示例中,fork函数被调用后,父进程和子进程都会继续执行下面的代码。输出结果显示了fork前后进程的PID变化,这有助于理解fork的工作机制。

fork函数返回值

fork函数中,返回值在父子进程中不同,这一点非常关键。子进程中返回0,而父进程中返回子进程的PID。这使得进程可以根据返回值来区分自己是父进程还是子进程,从而执行不同的逻辑。

子进程返回0,这是因为子进程是由父进程克隆出来的,初始时它的环境与父进程相同,但它从fork调用的返回点开始独立运行。这种机制允许子进程进行独立的操作,而不会影响父进程。

父进程返回子进程的PID,这是为了让父进程可以管理和控制子进程。通过子进程的PID,父进程可以监控子进程的状态、发送信号、等待子进程结束等操作。如果fork调用失败,返回-1,表示无法创建新进程,通常是由于系统资源不足或达到进程数量限制。

写时拷贝

在进程创建过程中,写时拷贝(Copy-On-Write,COW)是一种优化技术。通常,父子进程共享相同的内存页面,直到有一个进程试图写入数据。这时,操作系统才会为该进程分配独立的内存页面,从而避免不必要的内存复制,提高效率。

写时拷贝的实现依赖于内存管理单元(MMU)和页面表。当一个进程试图写入共享页面时,MMU会捕获写操作,并触发页面错误。操作系统内核会处理这个错误,为进程分配新的物理内存,并更新页面表,以确保写操作在独立的内存区域进行。

这种技术在提高系统性能和节省内存资源方面发挥了重要作用。例如,当一个进程创建子进程时,操作系统不需要立即复制整个进程的内存空间,而是通过写时拷贝机制在实际需要时才进行复制。

fork常规用法

在实际应用中,fork函数有两种常见用法。首先,父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程可以作为服务器进程,等待客户端请求,当有请求到达时,通过fork创建子进程来处理该请求,从而实现并发处理。

另一个常见用法是执行不同的程序。例如,父进程调用fork创建子进程后,子进程可以通过exec函数族执行一个全新的程序,而父进程继续执行原来的任务。这种方式常用于shell等命令解释器中,用户输入命令后,shell创建子进程执行该命令,而父进程继续等待下一个命令。

#include <unistd.h>
#include <stdio.h>

int main(void) {
    pid_t pid;

    if ((pid = fork()) == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // 子进程执行的代码
        printf("This is the child process, pid is %d\n", getpid());
    } else {
        // 父进程执行的代码
        printf("This is the parent process, pid is %d, child pid is %d\n", getpid(), pid);
    }

    return 0;
}

在这个示例中,父进程和子进程根据fork的返回值来执行不同的代码段,实现并发执行。

fork调用失败的原因

尽管fork函数非常强大,但在某些情况下可能会调用失败,常见的原因有以下几点:

  1. 系统中有太多的进程:操作系统对同时运行的进程数量有一定的限制。当系统中已经运行了太多的进程时,再次调用fork可能会失败。
  2. 用户进程数量超过限制:操作系统对每个用户可以创建的进程数量也有限制。如果当前用户已经创建了过多的进程,fork调用也可能会失败。

fork调用失败时,函数返回-1,并设置errno变量以指示具体的错误原因。常见的错误包括EAGAIN(系统限制或用户限制导致的资源不足)和ENOMEM(内存不足)。

了解这些原因并采取适当的措施可以避免或减少fork调用失败的情况。例如,通过监控系统资源和进程数量,及时释放不必要的进程,可以提高fork调用的成功率。

进程终止
进程退出场景

进程的生命周期终止有多种原因,通常可以分为正常终止和异常终止两类。正常终止是指进程按照预期完成任务并退出,异常终止则是进程在运行过程中遇到错误或被外部信号强制终止。

正常终止的场景包括:

  1. 代码运行完毕,结果正确:进程按照设计完成了所有任务,正常退出。
  2. 代码运行完毕,结果不正确:进程完成了任务,但结果不符合预期。这种情况下,进程依然正常退出,只是结果不如预期。

异常终止的场景包括:

  1. 代码异常终止:进程在执行过程中遇到不可预知的错误,例如段错误(segmentation fault)或非法指令。
  2. 外部信号终止:进程被用户或其他进程发送的信号(如SIGKILLSIGTERM)强制终止。
进程常见退出方法

进程的退出方法有多种,包括正常终止和异常终止。正常终止是通过程序控制来实现的,而异常终止通常是由于未处理的错误或外部干预。

正常终止

正常终止的几种方式如下:

  1. main函数返回:这是最常见的退出方法,main函数的返回值将作为进程的退出状态。
  2. 调用exit函数:exit函数执行一些清理工作后终止进程。
  3. 调用_exit函数:_exit函数立即终止进程,不进行清理工作。

示例:

#include <stdlib.h>
#include <stdio.h>

int main() {
    printf("Program is exiting normally\n");
    return 0; // 从main函数返回
}

int another_function() {
    printf("Program is exiting using exit()\n");
    exit(0); // 调用exit函数
}

int another_function_2() {
    printf("Program is exiting using _exit()\n");
    _exit(0); // 调用_exit函数
}
异常终止

异常终止的几种情况如下:

  1. 使用信号终止:例如,用户按下Ctrl+C组合键会发送SIGINT信号终止进程。
  2. 程序遇到未处理的错误:例如,访问非法内存地址

会导致段错误,进程被操作系统强制终止。

示例:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void signal_handler(int signal) {
    printf("Received signal %d, exiting...\n", signal);
    exit(1);
}

int main() {
    signal(SIGINT, signal_handler); // 捕获SIGINT信号

    printf("Running... Press Ctrl+C to terminate\n");
    while (1) {
        sleep(1); // 无限循环
    }

    return 0;
}
_exit函数

_exit函数是一个系统调用,用于立即终止进程,不进行任何清理工作。与exit函数不同,_exit函数不会调用用户定义的清理函数,也不会刷新标准IO缓冲区。

#include <unistd.h>

void _exit(int status);

参数status定义了进程的终止状态,父进程可以通过waitwaitpid获取该值。尽管status是一个整数,但仅有低8位可以被父进程所用。所以,当调用_exit(-1)时,在终端执行$?会发现返回值是255。

示例:

#include <unistd.h>
#include <stdio.h>

int main() {
    printf("This message will not be displayed\n");
    _exit(0);
    printf("This message will also not be displayed\n");
    return 0;
}

在这个示例中,_exit函数立即终止进程,后续的printf语句不会执行。

exit函数

exit函数用于正常终止进程,调用时会进行一些清理工作,如调用通过atexiton_exit注册的清理函数,刷新标准IO缓冲区,并最终调用_exit终止进程。

#include <stdlib.h>

void exit(int status);

示例:

#include <stdio.h>
#include <stdlib.h>

void cleanup(void) {
    printf("Cleanup function called\n");
}

int main() {
    atexit(cleanup); // 注册清理函数
    printf("Program is exiting using exit()\n");
    exit(0);
}

在这个示例中,exit函数会先调用已注册的清理函数cleanup,然后终止进程。

return退出

return语句是从main函数返回时使用的退出方法。实际上,return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值作为exit的参数。

示例:

#include <stdio.h>

int main() {
    printf("Program is exiting using return\n");
    return 0;
}

在这个示例中,return语句从main函数返回,并终止进程。

进程等待
进程等待必要性

在进程的生命周期中,子进程退出后,父进程需要通过某种机制获取子进程的退出状态并回收其资源。如果父进程不处理子进程的退出,子进程的退出信息将保留在系统中,导致“僵尸进程”的产生。僵尸进程会占用系统资源,最终可能导致系统性能下降甚至崩溃。

此外,父进程还需要通过进程等待机制来了解子进程的执行情况,例如子进程是否正常退出,退出码是什么,以及是否发生异常终止。这些信息对父进程的后续处理和资源管理非常重要。

进程等待的方法

Linux提供了两种主要的进程等待方法:waitwaitpid

wait方法

wait函数用于等待任一子进程退出,并回收其资源。调用wait时,如果有多个子进程,系统会选择一个已退出的子进程进行处理。如果所有子进程都在运行,wait将阻塞,直到有子进程退出。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

pid_t wait(int *status);

返回值:

  • 成功返回被等待进程的PID
  • 失败返回-1
waitpid方法

waitpid函数提供了更多的控制选项,允许父进程等待指定的子进程或设置非阻塞等待模式。通过参数pidoptions,父进程可以灵活地等待子进程。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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

参数:

  • pid:指定等待的子进程PID
    • pid = -1:等待任一子进程,等效于wait
    • pid > 0:等待指定PID的子进程
  • status:子进程退出状态
  • options:等待选项,如WNOHANG(非阻塞等待)

返回值:

  • 成功返回被等待进程的PID
  • 失败返回-1

示例:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    if ((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程执行的代码
        sleep(5);
        exit(0);
    } else {
        // 父进程等待子进程
        wait(&status);
        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        } else {
            printf("Child terminated abnormally\n");
        }
    }

    return 0;
}
获取子进程status

waitwaitpidstatus参数是一个输出型参数,由操作系统填充。通过位图解析子进程的退出状态,可以获取子进程的具体退出信息。

常用的宏定义包括:

  • WIFEXITED(status):如果子进程正常终止,返回非零值。
  • WEXITSTATUS(status):如果WIFEXITED非零,返回子进程的退出码。
  • WIFSIGNALED(status):如果子进程因信号而终止,返回非零值。
  • WTERMSIG(status):如果WIFSIGNALED非零,返回导致子进程终止的信号编号。

示例:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    if ((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程执行的代码
        sleep(5);
        exit(10);
    } else {
        // 父进程等待子进程
        wait(&status);
        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child terminated by signal %d\n", WTERMSIG(status));
        } else {
            printf("Child terminated abnormally\n");
        }
    }

    return 0;
}

在这个示例中,父进程通过wait获取子进程的退出状态,并使用宏定义解析子进程的退出信息。

具体代码实现

通过前面的知识,我们可以实现一个进程的阻塞等待和非阻塞等待的例子。

阻塞等待

阻塞等待意味着父进程会一直等待,直到指定的子进程退出。这种方式适用于父进程必须等待子进程完成任务的场景。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    if ((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程执行的代码
        printf("Child process is running, pid: %d\n", getpid());
        sleep(5);
        exit(0);
    } else {
        // 父进程阻塞等待子进程
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child terminated by signal %d\n", WTERMSIG(status));
        } else {
            printf("Child terminated abnormally\n");
        }
    }

    return 0;
}
非阻塞等待

非阻塞等待意味着父进程可以在等待子

进程的同时继续执行其他任务。这种方式适用于父进程需要处理并发任务的场景。

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    if ((pid = fork()) == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程执行的代码
        printf("Child process is running, pid: %d\n", getpid());
        sleep(5);
        exit(0);
    } else {
        // 父进程非阻塞等待子进程
        do {
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret == 0) {
                printf("Child is still running...\n");
                sleep(1);
            } else if (ret == -1) {
                perror("waitpid");
                exit(1);
            }
        } while (pid != -1 && !WIFEXITED(status) && !WIFSIGNALED(status));

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child terminated by signal %d\n", WTERMSIG(status));
        } else {
            printf("Child terminated abnormally\n");
        }
    }

    return 0;
}
进程程序替换
替换原理

在创建子进程后,子进程通常需要执行与父进程不同的任务。为此,子进程可以调用exec函数族以执行另一个程序。当进程调用exec函数时,该进程的用户空间代码和数据被新程序完全替换,进程ID保持不变,从新程序的入口点开始执行。

替换函数

Linux提供了六种以exec开头的函数,用于程序替换。这些函数的主要区别在于参数传递方式和环境变量的处理方式。

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释

这些函数如果调用成功,则不会返回,因为进程已经被新程序替换。如果调用出错,则返回-1,并设置errno指示错误原因。

函数命名理解
  • l(list):表示参数采用变长参数列表形式
  • v(vector):表示参数采用数组形式
  • p(path):表示文件路径可以通过环境变量PATH搜索
  • e(environment):表示可以指定环境变量
exec调用举例
#include <unistd.h>
#include <stdio.h>

int main() {
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

    // 直接指定路径
    execl("/bin/ps", "ps", "-ef", NULL);

    // 通过环境变量PATH搜索
    execlp("ps", "ps", "-ef", NULL);

    // 指定环境变量
    execle("/bin/ps", "ps", "-ef", NULL, envp);

    // 参数通过数组传递
    execv("/bin/ps", argv);

    // 通过环境变量PATH搜索,参数通过数组传递
    execvp("ps", argv);

    // 指定环境变量,参数通过数组传递
    execve("/bin/ps", argv, envp);

    return 0;
}

在这个示例中,不同的exec函数被调用以执行ps命令。通过这些示例可以看到,exec函数族提供了丰富的功能,以满足各种需求。

实现一个简易的shell

基于前面介绍的进程创建和程序替换知识,我们可以实现一个简易的shell。shell的主要功能是读取用户输入的命令,解析命令,创建子进程执行命令,并等待子进程结束。

实现思路如下:

  1. 获取命令行输入
  2. 解析命令行参数
  3. 创建子进程(fork
  4. 替换子进程执行用户命令(execvp
  5. 父进程等待子进程退出(waitpid

实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#define MAX_CMD 1024
char command[MAX_CMD];

int get_command() {
    memset(command, 0x00, MAX_CMD);
    printf("minishell$ ");
    fflush(stdout);
    if (scanf("%[^\n]%*c", command) == 0) {
        getchar();
        return -1;
    }
    return 0;
}

char **parse_command(char *buff) {
    int argc = 0;
    static char *argv[32];
    char *ptr = buff;

    while (*ptr != '\0') {
        if (!isspace(*ptr)) {
            argv[argc++] = ptr;
            while (!isspace(*ptr) && *ptr != '\0') {
                ptr++;
            }
        } else {
            while (isspace(*ptr)) {
                *ptr = '\0';
                ptr++;
            }
        }
    }
    argv[argc] = NULL;
    return argv;
}

int execute_command(char *buff) {
    char **argv = parse_command(buff);
    if (argv[0] == NULL) {
        return -1;
    }

    int pid = fork();
    if (pid == 0) {
        execvp(argv[0], argv);
        perror("execvp");
        exit(1);
    } else {
        waitpid(pid, NULL, 0);
    }

    return 0;
}

int main(int argc, char *argv[]) {
    while (1) {
        if (get_command() < 0) {
            continue;
        }
        execute_command(command);
    }
    return 0;
}

在这个示例中,shell通过循环获取用户输入的命令,解析命令参数,创建子进程执行命令,并等待子进程结束。这种简易的shell实现了基本的命令行解释功能。

总结

本文详细介绍了Linux操作系统中的进程控制,包括进程的创建、终止、等待以及程序替换等内容。
通过对这些知识的学习和实践,可以深入理解Linux进程管理的原理和机制,并能够应用于实际开发中。
嗯,就是这样啦,文章到这里就结束啦,真心感谢你花时间来读。
觉得有点收获的话,不妨给我点个吧!
如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在评论里提醒一声~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天进步亿丢丢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值