济源网站建设电话,阿里云服务器搭建多个网站,怎么创建一个博客网站,一键生成各种app软件在计算机操作系统中#xff0c;进程#xff08;Process#xff09;是一个非常重要的概念。进程控制是操作系统的核心功能之一#xff0c;对于Linux操作系统尤其如此。本文将详细介绍Linux操作系统中的进程控制#xff0c;从入门到精通#xff0c;涵盖进程的创建、终止、等…在计算机操作系统中进程Process是一个非常重要的概念。进程控制是操作系统的核心功能之一对于Linux操作系统尤其如此。本文将详细介绍Linux操作系统中的进程控制从入门到精通涵盖进程的创建、终止、等待以及程序替换等内容。
进程创建
fork函数初识
在Linux中fork函数是创建新进程的最重要函数之一。通过fork函数一个已存在的进程可以创建一个新进程。新进程称为子进程而原进程称为父进程。这个过程在操作系统的进程管理中起着至关重要的作用。
当一个进程调用fork函数时操作系统会进行一系列复杂的操作包括分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表中最终返回到用户空间开始调度器调度。
fork函数的关键在于它在父进程和子进程中分别返回不同的值。在父进程中fork返回子进程的PID而在子进程中fork返回0。如果fork调用失败则返回-1。这使得父进程和子进程可以根据返回值来执行不同的代码从而实现并发执行。
#include unistd.h
#include stdio.hint 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-WriteCOW是一种优化技术。通常父子进程共享相同的内存页面直到有一个进程试图写入数据。这时操作系统才会为该进程分配独立的内存页面从而避免不必要的内存复制提高效率。
写时拷贝的实现依赖于内存管理单元MMU和页面表。当一个进程试图写入共享页面时MMU会捕获写操作并触发页面错误。操作系统内核会处理这个错误为进程分配新的物理内存并更新页面表以确保写操作在独立的内存区域进行。
这种技术在提高系统性能和节省内存资源方面发挥了重要作用。例如当一个进程创建子进程时操作系统不需要立即复制整个进程的内存空间而是通过写时拷贝机制在实际需要时才进行复制。
fork常规用法
在实际应用中fork函数有两种常见用法。首先父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程可以作为服务器进程等待客户端请求当有请求到达时通过fork创建子进程来处理该请求从而实现并发处理。
另一个常见用法是执行不同的程序。例如父进程调用fork创建子进程后子进程可以通过exec函数族执行一个全新的程序而父进程继续执行原来的任务。这种方式常用于shell等命令解释器中用户输入命令后shell创建子进程执行该命令而父进程继续等待下一个命令。
#include unistd.h
#include stdio.hint 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.hint 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函数
}异常终止
异常终止的几种情况如下
使用信号终止例如用户按下CtrlC组合键会发送SIGINT信号终止进程。程序遇到未处理的错误例如访问非法内存地址
会导致段错误进程被操作系统强制终止。
示例
#include signal.h
#include stdio.h
#include unistd.hvoid signal_handler(int signal) {printf(Received signal %d, exiting...\n, signal);exit(1);
}int main() {signal(SIGINT, signal_handler); // 捕获SIGINT信号printf(Running... Press CtrlC to terminate\n);while (1) {sleep(1); // 无限循环}return 0;
}_exit函数
_exit函数是一个系统调用用于立即终止进程不进行任何清理工作。与exit函数不同_exit函数不会调用用户定义的清理函数也不会刷新标准IO缓冲区。
#include unistd.hvoid _exit(int status);参数status定义了进程的终止状态父进程可以通过wait或waitpid获取该值。尽管status是一个整数但仅有低8位可以被父进程所用。所以当调用_exit(-1)时在终端执行$?会发现返回值是255。
示例
#include unistd.h
#include stdio.hint 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.hvoid exit(int status);示例
#include stdio.h
#include stdlib.hvoid 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.hint 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.hpid_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.hpid_t waitpid(pid_t pid, int *status, int options);参数
pid指定等待的子进程PID pid -1等待任一子进程等效于waitpid 0等待指定PID的子进程 status子进程退出状态options等待选项如WNOHANG非阻塞等待
返回值
成功返回被等待进程的PID失败返回-1
示例
#include sys/types.h
#include sys/wait.h
#include stdio.h
#include stdlib.h
#include unistd.hint 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.hint 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.hint 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.hint 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.hint 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指示错误原因。
函数命名理解
llist表示参数采用变长参数列表形式vvector表示参数采用数组形式ppath表示文件路径可以通过环境变量PATH搜索eenvironment表示可以指定环境变量
exec调用举例
#include unistd.h
#include stdio.hint main() {char *const argv[] {ps, -ef, NULL};char *const envp[] {PATH/bin:/usr/bin, TERMconsole, 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进程管理的原理和机制并能够应用于实际开发中。 嗯就是这样啦文章到这里就结束啦真心感谢你花时间来读。 觉得有点收获的话不妨给我点个赞吧 如果发现文章有啥漏洞或错误的地方欢迎私信我或者在评论里提醒一声~