前言
本篇文章来讲解Linux中的进程,进程在Linux中是非常重要的一个知识点,掌握好进程是非常重要的。
一、进程的概念
在计算机科学中,进程(Process)是操作系统对正在运行的程序的一种抽象概念。进程可以被看作是一个正在执行的程序的实例。
每个进程都有自己的内存空间(包括代码、数据和堆栈等),它们独立地运行,并且相互之间不会干扰或访问彼此的内存。进程是操作系统进行资源分配和管理的基本单位,它拥有自己的一组系统资源,如处理器时间、内存、文件描述符、打开的文件等。
进程的主要特征包括:
独立性:每个进程都是相互独立的,它们运行在自己的内存空间中,相互之间不干扰,互不影响。
资源拥有:每个进程拥有一组资源,包括处理器时间、内存、文件描述符等。操作系统负责为进程分配和管理这些资源。
执行状态:进程可以处于运行、等待、就绪或终止等不同的执行状态。运行状态表示进程正在执行,等待状态表示进程暂时无法继续执行,就绪状态表示进程已准备好运行但尚未被调度,终止状态表示进程已经完成任务或异常终止。
进程间通信(IPC):进程可以通过各种方式进行通信和协作,如共享内存、文件、管道、消息队列、套接字等方法,实现数据交换和同步。
操作系统通过进程调度算法来管理和调度进程,以便公平地分配系统资源,并确保系统的稳定性和高效性。进程的创建、销毁、通信和同步是计算机系统中重要的组成部分,它们为应用程序提供了一个多任务执行的环境,并支持并发性和多用户操作。
二、进程的生命周期
在Linux中,进程的生命周期包括四个主要阶段:创建(Creation)、运行(Running)、等待(Waiting)和退出(Termination)。下面是对每个阶段的详细描述:
1.创建(Creation): 进程的创建是通过调用 fork 系统调用来实现的。fork 调用会创建一个新的进程(子进程),该子进程是原始进程(父进程)的副本。在创建过程中,子进程获得了父进程的代码、数据和打开的文件描述符等信息。
2.运行(Running): 进程在创建后进入运行阶段。在这个阶段,进程处于活动状态,执行其指定的任务。进程可以占用 CPU 资源并执行相应的操作。进程可能会在执行过程中被抢占或者主动放弃 CPU 资源,以便让其他进程执行。进程可以在用户空间或内核空间运行。
3.等待(Waiting): 进程可以由于某些条件不满足而暂时进入等待状态。在等待状态下,进程停止执行,并将 CPU 资源让给其他进程。进程可以等待多个事件,如等待信号、等待 I/O 完成等。当等待事件发生时,进程将被唤醒并重新进入运行状态。
4.退出(Termination): 进程在完成任务、显式终止或出现异常情况时会退出。进程可以通过调用 exit 系统调用来正常终止,并返回一个退出状态码给父进程。进程的退出时,它的资源(如内存、文件描述符)会被释放,同时系统记录进程的退出状态。
此外,进程的生命周期还受到其他因素的影响,如进程间通信(IPC)和信号处理等。进程可以与其他进程进行通信,共享资源,并根据特定事件或信号做出相应的处理。
需要注意的是,进程的生命周期可能因为特定需求或操作系统策略而有所变化。例如,守护进程(daemon process)通常会在后台长时间运行,而临时进程(ephemeral process)可能只在短暂的时间内存在。此外,进程的创建、运行、等待和退出可以在多个线程执行的上下文中发生,从而创建出多线程的进程。
三、进程树
进程树(Process Tree)是指在一个操作系统中,进程之间的层级关系形成的树状结构。在进程树中,每个进程都有一个父进程,除了根节点(通常是操作系统内核进程或init进程)的父进程为null,其他进程都有且只有一个父进程。而每个进程也可以有多个子进程。
进程树的形成是由于进程的创建和终止操作。当一个进程创建另一个子进程时,子进程成为新创建进程的子节点,而原始进程成为其父进程。这种方式创建了进程的层级结构,以及父子关系。当一个进程终止时,其子进程也可能会随之终止,进一步影响整个进程树。
进程树的一些重要特点和用途包括:
1.层级结构:进程树形成一个明确的层级结构,可以通过查找父进程和子进程来确定进程之间的关联。
2.进程组织:进程树可以将进程有序地组织起来,使操作系统能够有效地管理和跟踪进程的状态和资源。
3.资源分配:进程树中的父进程可以向其子进程分配资源,如文件描述符、内存等。
4.进程终止:当一个父进程终止时,它的子进程可能会成为孤儿进程,被操作系统接管,并由init进程接收,保证整个系统的稳定性。
5.进程通信:进程树中的父子关系可以方便地进行进程间的通信和协作,使用IPC机制进行数据交换和同步。
进程树的可视化表示通常以树状图的形式展现,根节点代表操作系统的内核进程或init进程,其下方的节点表示其他进程,子进程通过连接线与父进程相连接。
总结来说,进程树提供了一个有层级结构的视图,展示了操作系统中进程之间的父子关系。它是理解进程之间相互关联、资源分配和协作的重要工具。
使用ps -ax命令可以查看到进程树:
在Linux系统中,1号进程通常称为init进程。init进程是所有其他进程的祖先进程,是整个进程树的根节点。它是在系统引导时由内核创建的,负责系统初始化和启动用户空间的服务和进程。
init进程的主要任务是完成系统的初始化,并启动其他用户空间的进程和服务。在系统引导过程中,内核会首先加载并运行init进程。init进程会执行系统的初始化脚本和配置文件,设置系统环境,并启动其他一些关键的进程和服务。它是从内核态切换到用户态的第一个进程。
当init进程启动并完成初始化后,它会进入用户态运行。用户态是指进程在操作系统中以普通用户的权限进行操作和执行任务。在用户态下,进程可以访问用户的资源,执行用户的指令,并与其他用户空间进程进行通信。
四、进程的创建
进程创建的具体步骤:
1.为子进程申请内存空间,并将父进程数据完全复制到子进程空间中。
2.两个进程中的程序执行位置完全一致。
调用fork()函数后父进程返回子进程PID,子进程返回0。
代码示例:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(void) { int pid = 0; if((pid = fork() != 0)) { printf("praent pid :%d\n", getpid()); } else { printf("praent pid :%d\n", getppid()); printf("current pid = %d\n", getpid()); } return 0; }
五、一个进程可以执行几个程序?
一个进程其实是可以执行一个或者多个程序的。
在Unix和类Unix系统中,通过调用execve或其他exec系列函数,一个进程可以加载并执行一个新的程序映像,将其替换当前进程的代码和数据,从而实现在同一个进程内执行多个应用程序的效果。execve函数接受一个新的程序文件路径作为参数,将该文件加载到当前进程的地址空间,并且开始执行新的程序。
当进程调用execve函数时,它的地址空间和资源将被新程序替换,包括代码段、数据段、堆栈、文件描述符等。这样,进程会开始执行新程序的入口点,原来的程序会停止执行。
需要注意的是,execve函数本身并不是创建新进程的函数,它是在已有的进程中执行一个新程序。通过在fork创建的子进程中调用execve函数,可以实现替换子进程原有程序的效果。这样,一个进程可以先通过fork创建新的子进程,然后在子进程中使用execve函数执行其他程序。
因此,通过调用execve或其他exec系列函数,一个进程可以在同一个进程内执行多个应用程序,每次调用execve都会替换当前进程的代码和数据,从而实现多个应用程序的执行。
注意:execve系统调用并不会创建新的进程
execve函数原型:
int execve(const char *filename, char *const argv[], char *const envp[]);
参数说明:
filename:要执行的程序文件的路径。
argv:一个空指针结尾的字符串数组,用于传递给执行的程序的命令行参数。数组的第一个字符串通常是程序的名称。
envp:一个空指针结尾的字符串数组,用于传递给执行的程序的环境变量。数组中的每个字符串都是一个形如"key=value"的环境变量设置。
返回值:
成功执行时,execve函数不会返回,因为它会将当前进程替换为新的程序映像。如果发生错误,execve函数返回-1,并设置errno变量来指示具体的错误信息。
execve函数的执行过程如下:
1.execve函数加载指定的程序文件(filename)到当前进程的地址空间。这通常涉及到读取可执行文件的内容到内存中,建立新的程序的代码段、数据段等内存映射。
2.execve函数设置新程序的入口地址,并将参数(argv和envp)传递给新程序,使得新程序可以访问这些参数。
3.execve函数开始执行新的程序,并取代当前进程的执行,新程序开始从入口地址开始执行。
fork.c:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #define EXE "test" int main(void) { int pid = 0; char* argv[2] = {EXE, NULL}; printf("begin\n"); printf("now pid : %d\n", getpid()); execve(EXE, argv, NULL); printf("end\n"); return 0; }
test.c
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(void) { printf("Hello World current pid :%d\n", getpid()); return 0; }
这里使用execve函数来加载test这个可执行程序:
执行结果:
从结果上来看execve函数并不会创建出一个新的进程。
六、子进程中调用execve函数
execve函数常用于在子进程中启动新的程序,例如在使用fork函数创建子进程后,子进程通过调用execve函数来执行其他程序,实现进程的程序替换。这是实现进程间通信和程序的动态加载的重要手段之一。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #define EXE "test" int main(void) { int pid = 0; char* argv[2] = {EXE, NULL}; printf("begin\n"); printf("now pid : %d\n", getpid()); if((pid = fork()) != 0) { //父进程 } else { //子进程 execve(EXE, argv, NULL); } printf("end\n"); return 0; }
执行结果:
总结
本篇文章就讲解到这里,下篇文章继续讲解进程的知识。