在计算机操作系统中,进程(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
函数非常强大,但在某些情况下可能会调用失败,常见的原因有以下几点:
- 系统中有太多的进程:操作系统对同时运行的进程数量有一定的限制。当系统中已经运行了太多的进程时,再次调用
fork
可能会失败。 - 用户进程数量超过限制:操作系统对每个用户可以创建的进程数量也有限制。如果当前用户已经创建了过多的进程,
fork
调用也可能会失败。
当fork
调用失败时,函数返回-1,并设置errno
变量以指示具体的错误原因。常见的错误包括EAGAIN
(系统限制或用户限制导致的资源不足)和ENOMEM
(内存不足)。
了解这些原因并采取适当的措施可以避免或减少fork
调用失败的情况。例如,通过监控系统资源和进程数量,及时释放不必要的进程,可以提高fork
调用的成功率。
进程终止
进程退出场景
进程的生命周期终止有多种原因,通常可以分为正常终止和异常终止两类。正常终止是指进程按照预期完成任务并退出,异常终止则是进程在运行过程中遇到错误或被外部信号强制终止。
正常终止的场景包括:
- 代码运行完毕,结果正确:进程按照设计完成了所有任务,正常退出。
- 代码运行完毕,结果不正确:进程完成了任务,但结果不符合预期。这种情况下,进程依然正常退出,只是结果不如预期。
异常终止的场景包括:
- 代码异常终止:进程在执行过程中遇到不可预知的错误,例如段错误(segmentation fault)或非法指令。
- 外部信号终止:进程被用户或其他进程发送的信号(如
SIGKILL
或SIGTERM
)强制终止。
进程常见退出方法
进程的退出方法有多种,包括正常终止和异常终止。正常终止是通过程序控制来实现的,而异常终止通常是由于未处理的错误或外部干预。
正常终止
正常终止的几种方式如下:
- 从
main
函数返回:这是最常见的退出方法,main
函数的返回值将作为进程的退出状态。 - 调用
exit
函数:exit
函数执行一些清理工作后终止进程。 - 调用
_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函数
}
异常终止
异常终止的几种情况如下:
- 使用信号终止:例如,用户按下
Ctrl+C
组合键会发送SIGINT
信号终止进程。 - 程序遇到未处理的错误:例如,访问非法内存地址
会导致段错误,进程被操作系统强制终止。
示例:
#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
定义了进程的终止状态,父进程可以通过wait
或waitpid
获取该值。尽管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
函数用于正常终止进程,调用时会进行一些清理工作,如调用通过atexit
或on_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提供了两种主要的进程等待方法:wait
和waitpid
。
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
函数提供了更多的控制选项,允许父进程等待指定的子进程或设置非阻塞等待模式。通过参数pid
和options
,父进程可以灵活地等待子进程。
#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
:指定等待的子进程PIDpid = -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
wait
和waitpid
的status
参数是一个输出型参数,由操作系统填充。通过位图解析子进程的退出状态,可以获取子进程的具体退出信息。
常用的宏定义包括:
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的主要功能是读取用户输入的命令,解析命令,创建子进程执行命令,并等待子进程结束。
实现思路如下:
- 获取命令行输入
- 解析命令行参数
- 创建子进程(
fork
) - 替换子进程执行用户命令(
execvp
) - 父进程等待子进程退出(
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进程管理的原理和机制,并能够应用于实际开发中。
嗯,就是这样啦,文章到这里就结束啦,真心感谢你花时间来读。
觉得有点收获的话,不妨给我点个赞吧!
如果发现文章有啥漏洞或错误的地方,欢迎私信我或者在评论里提醒一声~