目录
[孤儿进程 (Orphan Process):](#孤儿进程 (Orphan Process):)
[僵尸进程 (Zombie Process):](#僵尸进程 (Zombie Process):)
[5. 进程映像的更新](#5. 进程映像的更新)
在Linux中,进程和线程是操作系统进行工作调度的基本单位,它们各自拥有特定的属性和作业环境。 进程是指在系统中正在运行的一个应用程序。每个进程间都有各自独立的内存空间,即使是同一个程序的不同实例也不会共享这些信息。比如说,你可以打开多个计算机浏览器,它们都是独立的进程。 每个进程至少有一个线程,这个线程被称为主线程。线程是进程中的一个单独的序列流,是进程的实际操作单位。线程在同一进程的上下文中运行,并共享同一进程的命名空间,这意味着它们共享同样的代码段、数据段和开放的文件资源等。 关于进程和线程之间的关系,你可以将进程看作是土地,而线程则像是在土地上进行工作的人。每块土地(进程)上可以有一个或多个工人(线程),他们共享土地资源(进程的内存空间和资源),但各自进行不同的工作。 总结一下,进程提供运行环境的上下文,而线程则在这个上下文中实际执行任务。线程的切换比进程的切换开销更小,因此使用多线程可以在同一进程内同时执行多项任务,提高程序的效率。
1.如何创建一个进程
在unix中我们使用fork函数来创建当前进程下的子进程:
fork()
是一个非常重要的系统调用。它被用来创建新的进程。这个新的进程被称为子进程,而创建子进程的进程被称为父进程。fork()
函数的独特之处在于,它只被调用一次,但却返回两次。它在父进程中返回新创建子进程的进程 ID,而在子进程中则返回 0。如果在调用fork()
期间发生错误,例如无法获取到必要的内核资源,那么就会返回一个负数。 当fork()
创建子进程时,子进程是父进程的一个复制品。它会获得父进程的数据空间、堆和栈的复制品。这意味着父进程中的数据和状态将会被复制到子进程中。然而,父进程和子进程的执行环境是独立的,他们有各自独立的进程ID,要传递信息需要使用特定的进程间通信(IPC)机制。 但是,值得注意的是,虽然子进程复制了父进程的上下文,但一些值,如一些系统资源(例如打开的文件描述符)、进程标识符(PID)、父进程标识符(PPID)等会被改变。 命名为fork()
的函数名代表着"分叉"的意思,恰当的描述了创建新进程的行为,即从一个进程分出一个新的进程,这两个进程之后将会独立执行。(查看手册)
使用例子:
cpp
#include<t_stdio.h>
#include <unistd.h>
int main(){
pid_t pid;
//在父进程中创建一个子进程
pid = fork();
if(pid==-1)E_MSG("fork",-1);
if(pid==0){//子进程执行的代码
printf("child process..\n");
}
else {//{父进程执行的代码
printf("parent process ..\n");
}
//父进程和子进程都能执行的代码
printf("all ok..\n");
return 0;
}
2.如何终止进程
我们常用return 和exit来终止一个进程:
使用例子:当前进程会停在gechar()这,这时我们查看我们当前进程与父进程的关系:
cpp
#include <stdlib.h>
#include<stdio.h>
int main(void){
getchar();
exit(1);
}
这里说明我们执行程序a.out的时候,其父进程是bash,我们使用exit(status)就会返回status给父进程:
2.2遗言函数
cpp
#include<t_stdio.h>
#include <stdlib.h>
#include <unistd.h>
void bye(void){
printf("bye bye..\n");
return;
}
void goodbye(void){
printf("good bye..\n");
return;
}
int main(void ){
//向进程注册遗言函数
atexit(bye);
atexit(bye);
atexit(goodbye);
//创建子进程
pid_t pid=fork();
if(pid==-1)E_MSG("fork ",-1);
getchar();
return 0;
}
先是父进程的遗言函数,如何是子进程的遗言函数。
3.进程资源的回收
cpp
#include<t_stdio.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/types.h>
int main(void ){
//创建子进程
int pid;
pid = fork();
if(pid==-1)E_MSG("fork .. ",-1);
if(pid==0){//子进程执行代码
printf("child process.. %d\n",getpid());
exit(0);
}
else {//父进程执行代码
//父进程阻塞等待回收子进程的资源
wait(NULL);
printf("parent process..\n");
}
return 0;
}
只有子进程被终止父进程才会终止:
同样还有另一个回收函数:
具体使用可以参见手册。
4.孤儿进程和僵尸进程
在Linux中,孤儿进程和僵尸进程是两种不同的状态,它们都与进程的生命周期和父子进程关系相关。下面我会分别解释这两种状态,并且提供一个代码例子进行详细分析。
孤儿进程 (Orphan Process):
当一个父进程终止或者退出时,它的子进程可能还在运行。这时,子进程将被 init 进程(进程号为1)收养。收养后的子进程会成为 init 进程的孩子,成为孤儿进程。孤儿进程不再有父进程,因此由 init 进程负责对其进行处理。
僵尸进程 (Zombie Process):
当一个进程完成了执行,但其父进程还没有通过 wait() 或 waitpid() 系统调用来获取其终止状态时,该进程会进入僵尸状态。僵尸进程的进程控制块仍然存在,但是进程已经没有任何执行代码,也没有分配到 CPU 时间片。僵尸进程不占用内存空间,但会占用一定的进程号(PID)资源。
代码示例:
下面是一个简单的 C 语言示例代码,演示了孤儿进程和僵尸进程的情况:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t child_pid = fork();
if (child_pid > 0) { // Parent process
printf("Parent process is running with PID: %d\n", getpid());
sleep(5); // Sleep for a while to allow child process to become orphan
printf("Parent process is terminating\n");
} else if (child_pid == 0) { // Child process
printf("Child process is running with PID: %d, Parent PID: %d\n", getpid(), getppid());
sleep(10); // Sleep for a while to become zombie
printf("Child process is terminating\n");
} else { // Error handling
perror("fork");
exit(EXIT_FAILURE);
}
return 0;
}
在这个例子中,父进程创建一个子进程,然后父进程会休眠5秒后退出。在此之前,子进程还在运行,但父进程已经退出,因此子进程会成为孤儿进程。而子进程会休眠10秒后退出,这时它会成为僵尸进程,因为父进程没有处理子进程的退出状态。
要观察进程状态,你可以使用
ps
命令,例如ps aux | grep <process_name>
,其中<process_name>
是你的程序的名称,或者使用top
命令观察进程列表。
5. 进程映像的更新
在Unix系统中,"进程映像更新"是一个与程序执行密切相关的概念。它通常指的是当一个进程调用 exec系列函数(例如: execv, execvp, execp, execl, execlp, execle等)时,它原本的程序映像(即内存中的代码和数据)会被新的程序映像所取代,这个过程就被称为"进程映像更新"。 一个典型的例子是通过 shell 运行命令。当你在 shell 中输入一个命令并且按下回车的时候,shell会创建一个子进程,并在这个子进程中调用 exec 来执行你输入的命令。而在 exec 执行完成后,这个子进程的映像就被替换为你输入命令的程序映像。
进程映像更新与父子进程关系密切,因为往往在子进程中执行 exec() 系列函数来代替原有程序。对于 bash shell 命令或者其他需要新建进程来执行的程序,都是新建了子进程,然后在子进程中执行执行 exec() 系列函数来替换新的程序。 下面这个 C 语言例子就是一个典型的父子进程与 exec() 函数相结合的例子,展示了这种动态的父子进程交替和进程映像更新:
cpp
#include<t_stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void){
//创建子进程
pid_t pid=fork();
if(pid==-1)E_MSG("fork",-1);
if(pid==0){
printf("child process..\n");
//更新进程的映像
execlp("ps","ps","-o","pid,ppid,pgrp,comm","NULL");
//只有exec执行失败了才会执行以下代码
perror("execlp");
exit(0);
}
else{
wait(NULL);
}
return 0;
}
这段代码首先创建了一个子进程,然后在子进程中替换原有进程映像以运行
ps
命令。父进程则等待子进程终止。 下面是我对各处代码的详细解释:
#include <t_stdio.h>
:这是一种非标准的头文件,可能是一个特定环境的定制头文件。通常我们使用标准库<stdio.h>
来进行输入输出操作。
#include <unistd.h>
:这个头文件提供了对POSIX操作系统 API 的访问。包括fork(), exec()等函数。
#include <sys/types.h>
和#include <sys/wait.h>
:这两个头文件用于支持进程控制相关的数据类型和函数。
pid_t pid = fork();
:使用fork()
函数创建了一个新的子进程。
if(pid == -1)E_MSG(\"fork\",-1);
:如果fork()
函数返回 -1,表示创建失败,此时调用E_MSG()
函数处理错误。
if(pid == 0){}
:如果fork()
函数返回值为 0,表示当前处于子进程环境。
execlp(\"ps\",\"ps\",\"-o\",\"pid,ppid,pgrp,comm\",\"NULL\");
:调用execlp()
函数在子进程中运行ps
命令。参数"ps"
是要执行的命令,接下来的参数是传递给ps
命令的参数。
"ps"
:表示要运行的是ps
命令,这个命令用于查看进程状态。"-o"
:这个参数用于指定ps
命令输出的格式。"pid,ppid,pgrp,comm"
:这是-o
参数后的具体格式,表示输出的是进程ID,父进程ID,进程组ID,以及命令名。"NULL"
:这应该表示参数列表的结尾,但"NULL"
这样的字符串并不会被execlp()
函数识别为参数列表结束,通常我们使用NULL
或者(char*) 0
来表示参数列表的结束。
perror(\"execlp\");
:如果execlp()
函数调用失败,则会执行到这条语句,显示错误信息。
exit(0);
:子进程结束运行,返回 0 表示正常退出。
wait(NULL);
:这是在父进程中的语句,父进程调用wait()
函数等待子进程结束。NULL
表示父进程不关心子进程的退出状态。 大致上,这个程序的作用就是创建一个子进程,然后在子进程中运行ps -o pid,ppid,pgrp,comm
命令,输出相关的进程信息,之后子进程退出,父进程在子进程退出后也结束运行。
exec() 函数实际上是一系列相关函数的函数家族(通称 exec 函数族或者 exec 系列函数),包括 exec, execp, execv, execvp, execve, execvpe 这些函数。这些函数主要用于在进程中加载和运行新的程序,它会替换当前进程的映像为新的程序映像,新程序从 main 函数开始执行,直到 main 函数返回或者调用 exit 结束。 下面我将逐一解释这些函数及其参数:exec(): exec 是其他函数的基础,但通常我们不直接使用它,而是使用它的一些封装版本。这个函数会替换当前进程映像为新的程序映像。
execv() 和 execvp(): 这两个函数接受两个参数。第一个参数是我们要运行的程序的路径。第二个参数是一个字符指针数组,该数组包含新程序的命令行参数,最后一个指针后面必须是一个 NULL 指针。这两个函数的区别在于, execvp() 会使用环境变量 PATH,去寻找我们要执行的程序。
execve() 和 execvpe(): 这两个函数比上面的函数多一个参数,即新程序的环境变量的列表。这个参数同样是一个字符指针数组,数组中的每个字符串都有 NAME=VALUE 的形式,最后一个指针后面必须是一个 NULL 指针。 execvpe() 同样会使用环境变量 PATH 去寻找程序。 总的来说, exec 系列函数用于在当前进程中启动新的程序,完全替代了原有的程序并重新开始执行。所以它通常用于实现程序间的切换。这些函数只有在出错时返回,如果正常执行,调用 exec 系列函数的代码之后的部分都不会被执行,因为已经被新程序替换了。如果出现错误,比如文件不存在或者权限不足等,那么就会返回 -1,并设置 errno 为相应的值来表示错误原因。