likes
comments
collection
share

进程探索:深入理解操作系统中的进程管理

作者站长头像
站长
· 阅读数 59

一.程序和进程

1.1程序

程序是一组按照特定顺序编写的指令的集合,它用于指导计算机执行某个特定任务或完成某种操作。程序可以通过编程语言来描述和实现,它定义了问题的解决方案,并告诉计算机如何执行这些解决方案。

程序的目的是通过一系列的指令来操作计算机的硬件和资源,以实现特定的功能。这些指令可以是算术运算、逻辑判断、循环控制、数据传输等等操作,它们被编写成代码的形式,能够被计算机识别和执行。

程序的开发过程通常包括以下几个阶段:

  1. 需求分析:确定程序要解决的问题或达到的目标,并明确用户的需求和期望。

  2. 设计:根据需求分析的结果,设计程序的整体架构和模块划分,确定算法和数据结构等。

  3. 编码:使用编程语言将设计好的算法和逻辑转化为计算机能够执行的指令。

  4. 调试:在编码完成后,通过测试和调试确保程序的正确性和稳定性,修复错误和问题。

  5. 部署和维护:将程序部署到目标环境中,用户可以开始使用程序。之后可能需要进行维护和升级,以适应新的需求和改进性能。

程序可以用于各种领域和应用,包括软件开发、数据分析、人工智能、网络通信、游戏开发等。它们形成了现代科技和计算机应用的基石,对社会生产和生活的方方面面都有着重要的影响。

程序由不同的字段组成,这些字段定义了程序的不同部分和属性。

常见的程序字段包括

  1. 代码段(Code Segment):也称为文本段,存储了程序的实际执行代码。代码段通常是只读的,包含了计算机执行的指令和函数。

  2. 数据段(Data Segment):也称为数据区,存储了程序运行时使用的全局变量和静态变量。

  3. BSS 段:BSS(Block Started by Symbol)段存储了未初始化的全局变量和静态变量。这些变量在程序启动时会被自动初始化为默认值,比如0。

  4. 堆(Heap):堆是动态分配内存的一部分,用于存储程序运行时动态创建的对象或数据结构。堆的大小可以根据需要自由扩展和收缩。

  5. 栈(Stack):栈用于存储程序的局部变量、函数参数和函数调用的上下文信息。栈是一种先进后出(FILO)的数据结构,用于支持函数的调用和返回。

除了上述字段之外,程序还可能包含其他字段,例如符号表(Symbol Table)用于记录变量和函数的名称与地址的映射关系,以及异常处理表(Exception Handling Table)用于处理程序异常情况等。

程序的字段和组织方式可以根据不同的编程语言和编译器进行不同的实现和优化。但是无论如何,这些字段都是为了帮助程序员更好地管理和组织代码以及实现程序的逻辑和功能。

1.2进程

进程是计算机中正在运行的程序的实例。每个进程都有自己的执行空间和资源,包括内存、文件句柄、CPU 资源等。进程是计算机进行任务调度和资源管理的基本单位。

一个进程可以包含一个或多个线程,每个线程在进程的上下文中运行,共享进程的内存和资源。线程是一个程序内的独立执行路径,它执行程序的一部分,并通过共享内存来与其他线程进行通信。

进程有以下几个主要特性:

  1. 独立性:每个进程是独立运行的,它们有自己的地址空间和资源,相互之间不会干扰。

  2. 并发性:多个进程可以同时存在并运行,操作系统通过任务调度算法来分配 CPU 资源,使多个进程能够并发执行。

  3. 资源管理:操作系统为每个进程分配和管理各种资源,包括内存、文件句柄、运行时间片等。

  4. 通信与同步:进程之间可以通过共享内存、消息传递等方式进行通信和同步,实现数据的共享和协作。

进程的生命周期通常包括以下几个阶段:

  1. 创建:当程序启动时,操作系统创建一个新的进程用于执行该程序。

  2. 就绪:进程已创建但暂时不执行,等待分配 CPU 资源。

  3. 运行:进程被操作系统选择为当前可执行的进程,并在 CPU 上执行。

  4. 阻塞:进程由于等待某些事件(例如等待输入/输出完成)而暂时无法继续执行,将进入阻塞状态。

  5. 挂起:操作系统可能会暂停进程的执行,并将其从内存中移动到磁盘上以节省资源。

  6. 终止:进程执行完成或发生错误时结束,并释放占用的资源。

操作系统通过进程调度算法来决定在某个时间点运行哪个进程,以实现多任务的并发执行。进程的管理和调度由操作系统负责,包括创建、终止、切换、通信等。进程的概念是计算机操作系统和并发编程的重要基础。

1.3进程和程序的区别

进程探索:深入理解操作系统中的进程管理

进程和程序是计算机中两个相关但不同的概念。

程序是一组按照特定顺序编写的指令集合,用于告诉计算机执行特定的任务。它是静态的,是代码的逻辑表示,通常保存在存储设备中,如硬盘或闪存。

进程是程序的执行实例,是计算机中正在运行的程序。它是动态的,具有独立的内存空间和资源。一个程序可以创建一个或者多个进程的实例。每个进程运行在自己的上下文中,具有自己的内存、寄存器、堆栈和文件句柄等资源。

因此,可以总结出程序和进程之间的区别如下:

  1. 定义:程序是静态的代码集合,而进程是程序在计算机上的运行实例。

  2. 动态性:程序是静态的,不执行任何操作。而进程是程序的执行状态,具有动态性。

  3. 资源和上下文:程序不占用计算机资源,而进程拥有独立的资源,如内存、文件句柄等,并且运行在自己的上下文中。

  4. 并发性:程序本身不能并发执行,但一个程序可以创建多个进程来实现并发执行。

  5. 生命周期:程序的生命周期从编写到存储,而进程的生命周期是在运行时创建、启动、执行、终止等过程。

综上所述,程序只是指定要执行的指令的代码集合,而进程是程序在计算机上运行时的实例,具有自己的资源和上下文。程序通过创建进程来实现在计算机上的实际执行。

1.4Linux中程序产生进程过程

流程 在 Linux 中,程序产生进程的过程可以从内核层面的 task_struct 数据结构来介绍,该数据结构用于表示进程的详细信息。下面是大致的步骤:

  1. 创建进程:当执行一个程序时,内核会通过调用 fork() 系统调用来创建一个新的进程。fork() 系统调用会将当前进程的 task_struct 数据结构复制一份,创建一个与父进程几乎相同的子进程。

  2. 分配进程 ID:在创建进程后,内核会为该进程分配一个唯一的进程 ID(PID)以标识该进程。进程 ID 在整个系统范围内是唯一的。

  3. 设置进程状态:新创建的进程的初始状态通常是就绪状态(RUNNING),表示它已准备好并可以开始执行。

  4. 分配资源:进程创建后,内核会为它分配必要的资源,包括内存空间、文件描述符、栈大小等。这些资源信息会保存在 task_struct 数据结构中。

  5. 设置上下文:进程创建后,内核会为它设置初始的执行上下文,包括程序计数器(PC)和其他寄存器的值。这样,当进程被调度时,它可以从正确的位置开始执行。

  6. 加入调度队列:新创建的进程会被添加到就绪队列中,等待调度器将其分配给一个 CPU 并开始执行。

  7. 执行程序:一旦进程被调度执行,它将开始执行程序的指令,按照程序的逻辑进行操作。

从 task_struct 数据结构的层次上看,Linux 中程序产生进程的过程包括创建进程、分配进程 ID、设置进程状态、分配资源、设置上下文、加入调度队列以及执行程序的步骤。这些步骤确保了进程的正确创建和执行。

mm(内存管理)

mm,即 Memory Management,是操作系统内核中的一个组件,主要负责管理和控制计算机的内存资源。它的作用是确保各个进程能够共享内存资源,并且按需分配和回收内存。

mm 的主要功能包括以下几个方面:

  1. 内存分配和回收:mm 负责管理内存的分配和回收。它通过内存管理算法,将可用的内存空间分配给进程,以满足其运行时的内存需求。当进程释放内存时,mm 将回收该内存,并重新将其标记为可用。

  2. 虚拟内存管理:mm 管理着进程的虚拟内存地址空间。它将虚拟地址映射到物理地址,并将内存分页成逻辑上连续的页帧,使得进程可以使用虚拟内存地址进行访问,而不需要了解物理内存的细节。

  3. 内存保护:mm 负责处理内存保护相关的任务,如内存访问权限管理、页面写保护、页面换入换出等。它通过页表和访问控制位,确保进程只能访问其拥有权限的内存区域。

  4. 内存共享和进程间通信:mm 提供了内存共享机制,使得不同进程可以共享同一块内存区域。这为进程间的通信和数据共享提供了便利。

  5. 内存清理和回收:当系统的内存资源不足时,mm 负责清理不再使用的内存页面,并将其释放回系统,以便其他进程使用。这确保系统能够高效地利用有限的内存资源。

mm 是操作系统内核的核心组件之一,负责管理和控制计算机的内存资源,以满足进程的内存需求,保护进程的内存访问权限,并提供内存共享和进程间通信的机制。

程序数据段存储在程序的虚拟内存中

程序中的数据会保存在进程的虚拟地址空间中的不同区域:

  1. 栈(Stack):栈是用于存储函数调用、局部变量和函数参数的一种数据结构。每当一个函数被调用时,相关的函数参数和局部变量就会被保存到栈帧中,并随着函数的执行在栈上动态地分配和释放空间。

  2. 堆(Heap):堆是用于动态分配内存的区域。在程序中使用诸如malloc、calloc或new等函数/运算符来分配内存时,这些分配的内存区域就位于堆中。堆的内存管理是通过系统调用(如brk和mmap)来实现的。

  3. 数据段(Data Segment):数据段存储全局变量和静态变量。它包括了已初始化的全局和静态变量的内存。数据段在程序静态分配内存时分配,并且在程序运行期间保持不变。

  4. BSS段(Block Started by Symbol):BSS段也存储全局变量和静态变量,但是这些变量在程序中未被显式初始化。BSS段在程序加载时会被初始化为零或空值。

总而言之,进程的虚拟地址空间中存在不同的区域用于存储程序的数据。栈用于保存函数调用和局部变量,堆用于动态分配内存,数据段存储已初始化的全局和静态变量,BSS段存储未显式初始化的全局和静态变量。我们可以童年过mm来管理虚拟内存。

1.5进程的死亡

进程的死亡过程通常可以分为以下几个步骤:

  1. 退出请求:当进程完成了它的任务或者发生了无法继续执行的错误时,它会向操作系统发送退出请求。这可以是显式的退出调用,如exit()函数,或者是其他非正常退出的情况,如错误导致的崩溃。

  2. 清理资源:在进程退出之前,它需要清理自己所占用的资源,包括关闭文件描述符、释放动态分配的内存、断开与其他进程的连接等。这些操作确保资源在进程终止后可以被回收并重新使用。

  3. 发送终止信号:进程在准备终止时,会向其父进程发送一个终止信号。这通知父进程进程正在退出,并且父进程可以作出相应的处理,如清理子进程的相关资源。

  4. 执行终止处理程序:在实际退出之前,进程会执行一些终止处理程序,这些程序可以由进程自身定义。例如,释放进程使用的其他资源、保存日志、发送通知等。

  5. 进程终止:最后,进程正式终止,并从操作系统的进程表中被移除。此时,进程的内存、打开的文件描述符、I/O缓冲区等会被系统回收和清理。

需要注意的是,进程的死亡过程也可能受到其他因素的影响,如操作系统的终止信号、内存不足等。在这些情况下,进程可能会被强制终止,而不经过正常的退出流程。

二.fork函数

2.1函数原型

fork()函数在 C 语言中的原型如下:

#include <unistd.h>

pid_t fork(void);

其中pid_t是一个整型数据类型,用于表示进程ID。fork()函数返回值是一个pid_t类型的值,具体含义如下:

  • 如果调用fork()的进程是父进程,则返回子进程的进程ID(PID)。
  • 如果调用fork()的进程是子进程,则返回0。
  • 如果出现错误,fork()返回-1。

通过检查fork()函数的返回值,可以判断当前代码是在父进程中还是在子进程中,并根据不同的返回值执行不同的代码逻辑。

需要注意的是,fork()函数需要包含头文件<unistd.h>才能进行调用。此外,在使用fork()函数时,应格外注意处理可能出现的错误情况。例如,当系统资源耗尽时,fork()可能会返回-1,表示创建子进程失败。

2.2fork函数功能

fork()函数的功能是创建一个新的子进程,该子进程是调用进程的几乎完全副本。具体功能和特点如下:

  1. 进程复制:fork()函数在调用进程中创建一个新的子进程,该子进程几乎完全复制了父进程的所有内容,包括代码、全局变量、堆、栈、文件描述符等。子进程是父进程的副本。

  2. 独立执行:fork()函数调用后,父进程和子进程分别继续执行,彼此之间的执行是相互独立的。它们有各自的内存空间和资源管理。

  3. 返回值区分:根据fork()函数的返回值,可以在父进程和子进程中执行不同的代码路径。在父进程中,fork()返回子进程的进程ID(PID),在子进程中,fork()返回0。可以根据返回值进行条件判断,以实现不同的代码逻辑。

  4. 进程间共享和隔离:父进程和子进程之间共享某些资源,如打开的文件描述符。这种共享机制可以用于进程间通信和共享状态。但同时,子进程是父进程的副本,它们之间的修改不会相互影响,各自拥有独立的虚拟内存空间。

通过使用fork()函数,可以实现以下功能:

  • 创建并发执行的多个进程,用于处理并行任务或任务分割。
  • 实现简单的进程间通信和共享资源。
  • 实现守护进程等特殊的进程模式。
  • 创建进程树,用于实现复杂的进程关系和层次结构。

需要注意的是,在实际使用fork()函数时,需要注意处理可能出现的错误情况,如资源耗尽或其他系统限制。

2.3fork函数特性

fork()函数具有以下几个特性:

  1. 创建子进程:fork()函数用于创建一个与父进程几乎完全相同的子进程。子进程从fork()函数的返回处开始执行,而父进程继续执行fork()之后的代码。

  2. 独立的执行环境:父进程和子进程在fork()后分别独立地执行,彼此之间的执行是相互独立的。它们有各自的内存空间和资源管理,所以它们的状态互不干扰。

  3. 返回值区分:根据fork()函数的返回值,可以在父进程和子进程中执行不同的代码逻辑。在父进程中,fork()返回子进程的进程ID(PID),在子进程中,fork()返回0。通过判断返回值,可以实现父子进程的不同分支逻辑。

  4. 共享和隔离的资源:父进程和子进程之间通过fork()函数共享某些资源,如打开的文件描述符。这意味着它们可以共享一些数据和状态。但同时,子进程是父进程的副本,它们之间的修改不会相互影响,各自拥有独立的虚拟内存空间。

  5. 进程树的形成:通过反复调用fork()函数,可以创建更多的子进程,从而形成进程树结构。子进程可以再次调用fork()创建更多的子进程,形成更复杂的进程关系和层次结构。

需要注意的是,在使用fork()函数创建子进程时,应当避免资源泄漏和竞争条件等问题,并妥善处理可能出现的错误情况。同时,对于父子进程之间的通信和同步,可以使用其他机制,如管道、共享内存、信号等。

2.4fork案例

下面是一个使用fork()函数创建子进程的简单示例:

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

int main() {
    pid_t pid;

    pid = fork();

    if (pid == -1) {
        // 创建子进程失败
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程代码
        printf("这是子进程(PID:%d)\n", getpid());
        printf("子进程结束\n");
    } else {
        // 父进程代码
        printf("这是父进程(PID:%d),创建了子进程(PID:%d)\n", getpid(), pid);
        printf("父进程结束\n");
    }

    return 0;
}

运行上述代码后,输出的结果可能如下:

这是父进程(PID:1234),创建了子进程(PID:1235
父进程结束
这是子进程(PID:1235
子进程结束

在这个案例中,程序首先调用了fork()函数,在父进程中会得到一个非负的子进程PID,而在子进程中则会得到0。之后,根据返回值不同,在父进程和子进程中分别输出不同的信息。最后,在每个进程中都输出进程结束的信息。

这个案例展示了fork()函数的基本用法,创建了一个父进程和子进程,它们具有相同的代码和执行流程,但是可以根据进程的不同,执行不同的代码路径。

三.exev函数族

3.1功能

exec()函数族的主要功能是在当前进程中运行一个新的程序。使用这些函数可以实现以下功能:

  1. 程序替换(Program Replacement):调用exec()函数后,当前进程的代码和数据会被新程序的代码和数据替换。这可以用于动态加载和替换程序,使得一个进程可以切换到运行不同的程序,实现灵活的程序执行。

  2. 命令行参数传递:exec()函数族提供了不同的方式传递命令行参数给新程序。可以使用列表或数组形式将参数传递给新程序。同时,还可以在某些函数中使用可 参数。

  3. 环境变量传递:部分exec()函数族提供了传递环境变量的功能。可以通过字符串数组形式设置新程序的环境变量。

  4. 程序搜索和执行:一些exec()函数(如execvp()execlp())会自动搜索可执行文件的路径,无需指定完整路径。它们会根据环境变量PATH中指定的路径,搜索并执行指定的程序。

  5. 进程间传递状态信息:通过执行新程序,可以将一些状态信息以特定的方式传递给新程序。这样可以在进程间共享数据和状态,并实现进程间的通信。

需要注意的是,exec()函数族在成功执行后,不会返回到原来的程序。新程序的代码开始执行,原来的进程被完全替换。因此,通常在exec()函数调用后紧跟exit()函数,以确保原来的程序在替换后退出。

3.2函数原型

exec()函数族的函数原型如下:

  1. int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

    • path:要执行的可执行文件的路径。
    • arg0:可执行文件的名称,作为参数传递给新程序。
    • 可变参数:新程序的命令行参数,以空指针(char *)0结尾。
  2. int execv(const char *path, char *const argv[]);

    • path:要执行的可执行文件的路径。
    • argv:新程序的命令行参数,以字符串数组形式传递,其中第一个字符串是可执行文件的名称。
  3. int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[] */);

    • path:要执行的可执行文件的路径。
    • arg0:可执行文件的名称,作为参数传递给新程序。
    • 可变参数:新程序的命令行参数,以空指针(char *)0结尾。
    • envp:新程序的环境变量,以字符串数组形式传递。
  4. int execvp(const char *file, char *const argv[]);

    • file:要执行的可执行文件的名称,不包含路径。
    • argv:新程序的命令行参数,以字符串数组形式传递,其中第一个字符串是可执行文件的名称。
  5. int execve(const char *file, char *const argv[], char *const envp[]);

    • file:要执行的可执行文件的路径。
    • argv:新程序的命令行参数,以字符串数组形式传递。
    • envp:新程序的环境变量,以字符串数组形式传递。

这些函数返回值为-1表示执行出错。成功执行exec()函数后,当前进程的代码和数据将被新程序的代码和数据替换,并开始执行新程序。

3.3后缀含义

exec()函数族有一些不同的后缀,用于表示不同的功能和参数形式。下面是一些常见的后缀:

  1. l:表示使用列表(list)形式的参数。接收一系列用空格分隔的命令行参数作为参数传递给新程序。

  2. v:表示使用数组(vector)形式的参数。接收一个字符串数组作为参数传递给新程序,其中包含程序名称和命令行参数。

  3. p:表示通过路径(path)搜索程序。会根据环境变量PATH中指定的路径搜索并执行指定的程序。

  4. e:表示接收环境变量(environment)参数。可以指定新程序运行时的环境变量,以字符串数组形式传递。

  5. x:表示接收完整的参数列表。除了接收程序路径和命令行参数外,还可以指定新程序运行时的环境变量。

这些后缀可以结合使用,来达到所需的功能和参数形式。例如,execlp()函数接收一个程序名称和一系列的命令行参数,并会在PATH环境变量指定的路径中搜索该程序进行执行。

需要注意的是,exec()函数族中并没有具有无后缀的exec()函数,因为这样的函数原则上无法确定参数列表的长度。所以,我们需要使用带有具体后缀的函数来适应不同的参数形式和需求。 3.4函数案例

execl:

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

int main() {
    printf("This is the original program\n");
    
    // 使用execl函数族调用ls命令
    execl("/bin/ls", "ls", "-l", NULL);
    
    printf("This line will not be executed\n");
    
    return 0;
}

execlp:

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

int main() {
    printf("This is the original program\n");
    
    // 使用execl函数族调用ls命令
    execlp("ls", "ls", "-l", NULL);
    
    printf("This line will not be executed\n");
    
    return 0;
}

execv:

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

int main() {
    printf("This is the original program\n");
    char *buf[]={ "ls", "-l", NULL};
    // 使用execl函数族调用ls命令
    execv("/bin/ls",buf);
    
    printf("This line will not be executed\n");
    
    return 0;

}

exeve

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

int main() {
    char *args[] = { "/path/to/my_executable", "arg1", "arg2", NULL };
    char *env[] = { "ENV_VAR1=value1", "ENV_VAR2=value2", NULL };
    
    printf("This is the original program\n");
    
    // 使用execve函数族执行自定义可执行文件,并传递环境变量
    execve("/path/to/my_executable", args, env);
    
    printf("This line will not be executed\n");
    
    return 0;
}

四.进程退出

4.1进程退出的几种方式

有几种方式可以使进程退出:

  1. 正常退出(Normal Exit):进程在执行完毕后,可以调用exit()函数来正常退出。exit()函数会执行一些清理操作,关闭文件描述符等,并将退出状态码传递给操作系统。通常,返回0表示进程成功退出,非零值表示进程出现错误。

  2. 异常退出(Abnormal Exit):进程在执行过程中,如果遇到无法继续运行的异常情况,可以选择异常退出。比如,通过调用abort()函数来终止程序,或者通过收到无法处理的信号而强行终止进程。异常退出会导致操作系统终止进程并产生一个称为核心转储(Core Dump)的调试文件,用于分析异常退出的原因。

  3. 子进程退出(Child Process Exit):进程可以使用fork()函数创建子进程,子进程完成工作后,通过调用exit()函数退出。这样,在父进程中可以通过wait()waitpid()函数等待子进程的退出并获取退出状态。父进程还可以通过WIFEXITEDWEXITSTATUS宏来判断子进程是正常退出还是异常退出,并获取退出状态码。

  4. 直接终止(Termination):进程可以调用_exit()函数或者使用系统调用exit_group()来直接终止进程。与exit()不同,直接终止不会执行清理操作,而是立即终止进程,不返回任何状态给操作系统。

需要注意的是,无论是正常退出、异常退出还是直接终止,进程退出后会释放其占用的系统资源,包括文件描述符、内存等。同时,父进程可以通过相关机制(如wait()函数)获取子进程的退出状态,以便进行进程间的通信和资源回收。

4.2exit和 _exit和return的区别

exit()_exit()return在进程退出方面有一些区别:

  1. exit()函数用于正常退出进程。当调用exit()函数时,它会执行一些清理操作(如关闭打开的文件描述符)并终止进程。它还通过返回一个退出状态码将控制权返回给操作系统。exit()函数可用于任意函数中,通过调用它可以使整个进程退出。

  2. _exit()系统调用是直接终止进程。当调用_exit()函数时,进程立即终止,而不执行任何清理操作。与exit()不同,_exit()函数不返回任何状态给操作系统,而是直接终止进程。通常,_exit()函数用于异常情况或在需要立即终止进程而不进行清理操作的情况下。

  3. return语句用于退出函数或方法,并将控制权返回到调用该函数或方法的位置。return语句只能用于函数或方法的内部,而不能使整个进程退出。当函数或方法中的所有代码都执行完毕或遇到return语句时,该函数或方法的执行将结束,控制权将返回给调用者。

需要注意的是,无论是exit()_exit()还是return,它们都会终止当前进程的执行。但是,exit()_exit()之间还存在一些差别,主要体现在清理操作和操作系统状态码的返回。

另外,需要注意的是,exit()_exit()都位于<unistd.h>头文件中,需要通过包含该头文件来使用这两个函数。而return是C和C++中的语言关键字,在函数或方法中直接使用即可。

4.3等待子进程退出

父进程回收子进程的主要原因有以下几点:

  1. 避免僵尸进程:当一个子进程先于父进程退出时,称为僵尸进程。僵尸进程不会被系统释放资源,占用系统的进程表项,如果产生大量的僵尸进程,可能会导致系统资源的浪费。父进程回收子进程可以避免僵尸进程的存在。

  2. 获取子进程的退出状态:通过回收子进程,父进程可以获取子进程的退出状态。这对于父进程来说可以根据子进程的状态来决定后续的操作,比如重新创建子进程、打印日志、发送通知等。

  3. 资源回收:当子进程创建了一些资源,如打开文件、分配内存等,在子进程退出后,这些资源可能需要被释放,以防止资源泄漏。父进程回收子进程时可以执行相应的资源回收操作,确保系统资源的合理利用。

为了回收子进程,父进程通常会使用wait()waitpid()函数。这些函数会阻塞父进程,直到一个子进程退出为止,并返回子进程的退出状态。父进程可以通过这些函数获取子进程的退出状态,并采取相应的措施。

需要注意的是,父进程只能回收它自己创建的直接子进程,而不能回收其他进程创建的子进程。此外,如果父进程不回收子进程,子进程退出后会成为孤儿进程,由init进程(进程ID为1)接管它们的回收工作。

wait函数

wait()函数是一个系统调用,用于父进程等待子进程的状态改变并获取子进程的退出状态。它可以阻塞父进程,直到一个子进程退出或被终止,然后返回子进程的进程ID和退出状态。

wait()函数的原型如下:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

其中,status是一个指向整型变量的指针,用于存储子进程的退出状态。

wait()函数的工作原理如下:

  1. 如果父进程没有正在运行的子进程,则wait()会阻塞父进程,直到有一个子进程退出或被终止。

  2. 如果父进程有一个或多个正在运行的子进程,则wait()会挂起父进程的执行,直到其中一个子进程退出或被终止。

  3. 当一个子进程退出时,wait()会返回子进程的进程ID,并将子进程的退出状态通过status指针传递给父进程。

  4. 如果父进程指定了status参数(即status不为NULL),则子进程的退出状态将被存储在status指向的位置。

wait()函数返回的进程ID可以用于进一步处理子进程的退出状态,比如进行资源回收、记录日志等。

需要注意的是,wait()函数只能等待直接子进程的状态改变,即子进程必须是wait()函数调用前的直接子进程。如果父进程希望等待任意子进程的状态改变,可以使用waitpid()函数,并将其第一个参数设置为-1

此外,wait()函数对于子进程的退出状态有一些宏定义,可以通过这些宏来判断子进程的退出状态,例如:

  • WIFEXITED(status) :如果子进程正常退出,则返回真。
  • WEXITSTATUS(status) :如果WIFEXITED为真,则返回子进程的退出状态码。
  • WIFSIGNALED(status) :如果子进程因收到信号而异常终止,则返回真。
  • WTERMSIG(status) :如果WIFSIGNALED为真,则返回导致子进程终止的信号编号。

通过这些宏定义可以方便地判断子进程的退出状态,并进行相应的处理。

示例 为了等待子进程退出,可以使用wait()waitpid()函数。下面是使用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;

    pid = fork(); // 创建子进程

    if (pid < 0) {
        // 创建子进程失败
        perror("fork error");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程逻辑
        printf("Child process with PID %d\n", getpid());
        sleep(3); // 模拟子进程执行一段时间
        printf("Child process exiting\n");
        exit(EXIT_SUCCESS);
    } else {
        // 父进程逻辑
        printf("Parent process with PID %d\n", getpid());
        printf("Waiting for child process with PID %d\n", pid);
        wait(&status); // 等待子进程退出
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process exited due to signal: %d\n", WTERMSIG(status));
        }
        printf("Parent process exiting\n");
        exit(EXIT_SUCCESS);
    }
}

在上面的示例中,父进程通过调用wait(&status)来等待子进程退出。wait()函数会阻塞父进程,直到一个子进程退出为止。status参数是一个指向整型变量的指针,用于存储子进程的退出状态。

父进程通过判断status来获取子进程的退出状态。使用WIFEXITED(status)宏可以检查子进程是否正常退出,如果返回真,则可以使用WEXITSTATUS(status)宏来获取子进程的退出状态码。如果子进程因接收到信号而异常终止,可以使用WIFSIGNALED(status)宏来检查,并使用WTERMSIG(status)宏来获取导致子进程终止的信号编号。

注意,在父进程等待子进程退出之前,子进程需要先执行适当的业务逻辑和退出步骤。在示例中,子进程会休眠3秒后退出。

waitpid函数

waitpid 是一个在操作系统中用来等待子进程结束的函数。它的原型如下:

#include <sys/types.h>
#include <sys/wait.h>

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

waitpid 的作用是等待指定进程结束,并返回该子进程的进程ID。它可以用来阻塞当前进程,直到子进程结束为止。该函数的参数包括:

  • pid:指定要等待的进程ID。
    • pid > 0:等待进程ID为 pid 的子进程。
    • pid = -1:等待任意子进程。
    • pid = 0:等待和当前进程在同一个进程组中的任意子进程。
    • pid < -1:等待进程组ID为 -pid 的任意子进程。
  • status:用于保存子进程的终止状态,它是一个指向 int 类型的指针。通过该指针,可以获取子进程的退出状态、终止信号等信息。
  • options:用于指定等待子进程结束的一些选项。
    • 指定为 0,表示默认行为,即阻塞等待子进程结束。
    • 指定为 WNOHANG,表示非阻塞等待子进程结束,即如果指定的子进程还未结束,立即返回。
    • 指定为 WUNTRACED,表示在子进程进入暂停执行(停止)状态后立即返回。
    • 可以使用 options 的位掩码组合多个选项。

waitpid 函数的返回值表示子进程的状态,它可以通过一些宏来判断子进程的状态,如:

  • WIFEXITED(status):如果子进程正常退出,返回真。
  • WEXITSTATUS(status):如果子进程正常退出,返回子进程的退出状态。
  • WIFSIGNALED(status):如果子进程是因为某个信号而终止的,返回真。
  • WTERMSIG(status):如果子进程是因为某个信号而终止的,返回终止信号的编号。
  • WIFSTOPPED(status):如果子进程暂停执行(停止),返回真。
  • WSTOPSIG(status):如果子进程暂停执行(停止),返回停止信号的编号。

总之,waitpid 函数提供了一种方便的方式来等待子进程结束,并获取子进程的退出状态或终止信号等信息。

五.进程转态及其转换过程

在 Linux 操作系统中,进程的状态可以相互转换,下面是不同状态之间的相互转换:

  1. 就绪态(Ready State):当一个进程创建后,它被放入就绪态。此时,进程已经被加载到内存中,并准备好被 CPU 分配时间片来执行。

  2. 运行态(Running State):当就绪态的进程获得 CPU 时间片后,进程的状态会从就绪态切换到运行态,并开始执行指令。

  3. 阻塞态(Blocked State):在运行过程中,如果进程需要等待某个事件的发生,例如等待键盘输入、等待 I/O 完成等,进程会从运行态进入阻塞态(也称为等待态)。此时,进程将暂停执行,直到等待的事件发生。

  4. 僵尸态(Zombie State):当一个进程已经完成执行并终止,但其父进程尚未对其进行资源回收,该进程会成为僵尸进程。僵尸进程仍然占用系统资源,但不再执行任何指令。

  5. 死亡态(Terminated State):一旦父进程对僵尸进程进行了适当的资源回收(通过调用 waitwaitpid 函数),僵尸进程将被完全终止并释放系统资源。

  6. 睡眠态(Sleep State):进程可以通过调用休眠函数(如 sleepusleep)或等待某个条件变量满足(如 pthread_cond_wait)来进入睡眠态。在这种状态下,进程将被挂起,直到指定的时间到达或条件满足为止。

  7. 暂停态(Suspended State):进程可以通过发送暂停信号(如 SIGSTOP)或调用 pause 函数来进入暂停态。在暂停态下,进程将被挂起,直到收到恢复执行的信号。

需要注意的是,僵尸进程是需要被及时回收的,否则会导致系统资源的浪费。父进程可以通过适当的方式(如调用 waitwaitpid 函数)回收子进程的资源,确保僵尸进程的及时清理。

进程探索:深入理解操作系统中的进程管理

六.进程组

6.1进程组

在Linux操作系统中,进程组(Process Group)是一组进程的集合。进程组内的每个进程都有一个相同的进程组ID(PGID)。进程组可以用于进行作业控制、信号传递和进程状态管理等操作。

每个进程组都有一个组长进程,其进程ID(PID)等于进程组ID(PGID)。通常,进程组的第一个进程会成为组长进程。

进程组主要有以下特点:

  1. 创建进程组:可以通过setpgid()系统调用将进程添加到指定的进程组或创建一个新的进程组。常见的方式是使用fork()exec()函数,子进程可以通过setpgid(0, 0)将自己加入到新进程组,或者使用setpgid(pid, pgid)将指定进程加入到现有进程组。

  2. 进程组ID:进程组ID(PGID)是一个非负整数,用于唯一标识一个进程组。进程组ID由操作系统分配并在进程组创建时指定。

  3. 进程组的作用:进程组常用于作业控制和信号传递。作业控制通过对进程组的操作来管理一组相关联的进程,例如发送信号给整个进程组、暂停或恢复进程组中的所有进程等。

  4. 进程组的关系:进程组不存在嵌套关系,每个进程组都是扁平的,不包含其他进程组。然而,一个进程可以同时属于多个进程组,即多个进程组ID与其PID相等。这在进行作业控制和信号传递时可能有用。

总而言之,进程组是一组拥有相同进程组ID的进程的集合。它们提供了一种对进程进行组织和管理的机制,并在作业控制、信号传递和进程状态管理等方面发挥重要作用。

6.2父进程和子进程

当进程调用了 fork() 函数创建子进程时,子进程会成为父进程的副本,包括进程组的关系。在默认情况下,父子进程会属于同一个进程组,且父进程作为进程组的组长。具体来说:

  • fork() 函数调用后,子进程将会获得父进程的副本,包括进程组ID(PGID)。

  • 父进程的进程组ID(PGID)不会改变,即父进程仍然是进程组的组长。

  • 子进程的进程组ID(PGID)将与父进程相同。

这意味着,父进程和子进程将属于同一个进程组,且父进程仍然是进程组的组长。

需要注意的是,在某些特定的情况下,父进程可能会在 fork() 后修改子进程的进程组ID(PGID),或者子进程可能会通过调用相关的系统调用来修改自己的进程组ID(PGID)。

总结起来,当进程调用了 fork() 函数创建子进程时,默认情况下,父子进程将属于同一个进程组,且父进程仍然是进程组的组长。子进程将继承父进程的进程组ID(PGID)。尽管如此,父进程和子进程仍然可以通过相关的系统调用修改自己的进程组ID(PGID)。

6.3shell的管道

在 shell 中通过管道连接的应用程序,确实会创建一个进程组,并且第一个程序成为进程组的组长。

当在 shell 中使用管道 | 连接两个应用程序时,shell 会创建一个子进程来执行第一个应用程序,并将其设为进程组的组长。然后,shell 创建另一个子进程来执行第二个应用程序,并将其加入到与第一个应用程序相同的进程组中。这样,两个应用程序就属于同一个进程组,且第一个应用程序成为该进程组的组长。

此设计的目的是为了能够对整个管道进行作业控制和信号传递。作业控制允许用户对一组相关联的进程进行统一管理,而信号传递可以在需要的时候将信号发送给整个进程组。

需要注意的是,由于管道中的进程是通过管道进行输入和输出的,它们之间的通信是通过内核缓冲区进行的,而不是直接的进程间通信。

总结起来,在 shell 中通过管道连接的应用程序,这两个程序属于同一个进程组,第一个程序成为进程组的组长,这样可以方便进行作业控制和信号传递。

6.4shell执行一个程序

在 shell 中直接执行一个应用程序时,对于大部分进程来说,它们自身成为一个独立的进程组,因此进程组中只包含一个进程。

当在 shell 中执行一个应用程序时,shell 会创建一个子进程来执行该应用程序。这个子进程成为一个独立的进程组,进程组ID(PGID)和进程ID(PID)相同。这意味着该进程是进程组中唯一的进程,也是进程组的组长。

在这种情况下,进程组的目的可能不是为了作业控制或信号传递,而是为了在执行过程中将其与其他进程分隔开,以获得更好的隔离性和独立性。

需要注意的是,这只适用于大部分的情况。在某些特殊的情况下,应用程序可能会以其他方式创建自己的进程组,或者与其他进程共享进程组。这种情况是因为应用程序的特殊需求而进行的特定处理。

总结起来,当在 shell 中直接执行一个应用程序时,这个应用程序通常会成为一个独立的进程组,进程组中只包含一个进程。这种设计可以提供进程的隔离性和独立性。但需要注意的是,在特殊情况下,应用程序可能会以其他方式创建进程组或与其他进程共享进程组。

七.会话与终端

7.1会话

会话(session)是操作系统中用于管理进程的抽象概念,它提供了一个运行环境和资源共享的上下文。会话包含了一组相关的进程,这些进程具有共同的会话标识符(session ID)。

在一个会话中,通常有一个特殊的进程被指定为会话首领(session leader)。会话首领是该会话的控制进程,它通常是由用户启动的程序(如shell)。会话首领的进程ID(PID)就是该会话的ID。

一个会话可以包含多个进程组(process group),每个进程组由一组进程构成。进程组是为了方便集中管理和控制一组相关联的进程。进程组有一个唯一的进程组ID(PGID),最常见的情况是进程组ID与会话ID相同。

会话的概念提供了一些重要的功能和特性:

  1. 进程间通信:会话中的进程可以通过进程间通信(IPC)机制相互通信和共享信息。常见的IPC方式包括管道、命名管道、消息队列、共享内存和信号。

  2. 控制终端:会话通常与一个控制终端相关联,这个终端用于输入和输出的交互。控制终端通常是一台终端设备或一个伪终端(pty)。会话首领可以通过控制终端与用户进行交互,并控制终端的行为。

  3. 作业控制:会话概念提供了作业控制的机制,即可以将进程从前台切换到后台,或者从后台切换到前台。前台作业是当前正在与用户交互的作业,而后台作业是在后台运行而无需用户交互的作业。使用作业控制命令(如bg、fg、jobs)可以操作作业的状态和行为。

  4. 会话注销:会话概念也与用户会话登录和注销相关。当用户登录时,会话被创建;当用户注销或会话首领进程退出时,会话被销毁。

总的来说,会话是操作系统提供给进程的一个抽象层,用于管理进程组、提供运行环境和资源共享。会话的核心组成部分是会话首领和进程组,通过控制终端、进程间通信和作业控制等机制,提供了进程间的交互和协作,会话是用来管理进程组

7.2会话的诞生

setsid函数 调用setsid函数可以创建一个新的会话,并将调用者(应用程序)作为新会话的首进程。新会话的首进程会成为新会话的领导者(session leader),即会话首进程。

具体步骤如下:

  1. 应用程序调用setsid函数。setsid函数通常会在子进程中调用,以确保子进程成为新会话的首进程。
  2. 如果调用setsid的进程不是一个进程组的首进程,那么会创建一个新的会话,并将调用者设置为会话的首进程。这将使得调用者脱离其父进程和原会话,并成为一个新的会话领导者。
  3. 新会话不会与任何控制终端关联。这是因为新会话的首进程没有从父进程继承终端的能力,所以会话中的进程无法与终端交互。
  4. 如果调用者之前已经是一个进程组的首进程,则setsid调用失败,返回-1。

通过调用setsid函数,应用程序可以创建一个新的独立会话,该会话没有与任何终端相关联,可以在后台运行,并且拥有一些会话特有的属性和权限。这在服务器程序、守护进程或后台任务中非常有用。

打开终端

当用户在终端正确登录后,Linux系统会创建一个新的会话,并将Shell进程作为会话的首进程(也可以称为会话首领)。这个过程通常是由登录管理器(如getty或login)处理的。

下面是创建新会话的一般过程:

  1. 用户通过终端正确登录,登录管理器验证用户的身份。
  2. 验证成功后,登录管理器会调用系统的登录处理程序来启动Shell进程。
  3. 登录处理程序会调用setsid函数,以创建一个新的会话并使Shell进程成为会话首进程。
  4. 新的会话中的Shell进程继承了会话首进程的进程组ID(PGID),会话ID(SID)和控制终端(如果有)。
  5. 会话首进程(即Shell进程)会接管控制终端,并作为用户与终端交互的主要接口。

通过将Shell进程设置为新会话的首进程,可以使Shell获得一些与会话相关的特性:

  • Shell进程可以接收和处理与控制终端相关的信号(如SIGHUP)。
  • Shell进程可以管理会话中其他进程组的控制终端访问权限。
  • Shell进程可以使用作业控制工具(如fg、bg、jobs)来管理会话中的进程组。
  • Shell进程作为会话首进程,可以方便地切换前台和后台进程组。

这种创建新会话的方式使得Shell进程具有更大的控制权和灵活性,能够管理会话中的进程并与用户进行交互。

终端与会话

在Linux系统中,会话(session)和终端之间的关系是通过控制终端设备(controlling terminal)来建立的。控制终端是一个字符设备,它与终端设备(如TTY)或伪终端(PTY)相关联。

当用户在终端正确登录后,系统会为其创建一个新的会话,并将该终端设备作为该会话的控制终端。通过控制终端,用户可以与该会话进行交互。

具体的过程如下:

  1. 用户在终端正确登录后,登录管理器会为其分配一个控制终端设备,该设备会在登录过程中打开。
  2. 然后,登录管理器使用exec函数族中的一个函数(如execle或execlp)来启动Shell进程。
  3. Shell进程成为会话的首进程,并继承控制终端的文件描述符。
  4. 用户在终端输入的命令将通过控制终端设备传递给Shell进程进行处理,Shell进程的输出也将通过控制终端设备显示给用户。

需要注意的是,一个会话可以有一个控制终端,但一个控制终端可以控制多个会话。例如,在多个终端中登录多个用户会话时,每个会话都会有自己的控制终端设备。

综上所述,会话与终端之间的关系是通过控制终端设备建立的。控制终端设备允许用户在终端上与会话进行交互,用户输入的命令和会话的输出都通过控制终端设备进行传输。再次对之前的回答错误表示诚挚的道歉,并感谢您的指正。如还有任何问题,请随时告知。

前台进程组

当Shell进程启动时,默认情况下会成为前台进程组的首进程,并占用与该前台进程组关联的控制终端来运行。

当Shell启动其他应用程序时,这些应用程序并不会成为新会话的首进程。相反,它们仍然是Shell进程的子进程,并继承了Shell进程的会话ID(SID)、进程组ID(PGID)和控制终端。

控制终端只在前台进程组的首进程中被占用。当Shell进程启动其他应用程序时,这些应用程序仍属于同一前台进程组,并与Shell进程共享控制终端。它们可以通过控制终端进行输入输出交互。

需要注意的是,一旦Shell进程在前台启动了一个作业(例如运行一个长时间运行的命令),该作业将占用控制终端。在此期间,Shell进程将无法使用控制终端进行输入输出,直到作业运行结束或被挂起。

总结来说,Shell进程在启动时,默认是前台进程组的首进程,并会占用会话相关联的控制终端。而Shell进程启动的其他应用程序仍然是Shell进程的子进程,并共享同一控制终端。

后台进程组

在Shell进程中,可以使用"&"符号来指定一个程序在后台运行。当程序在后台运行时,它将成为一个后台进程,并不会占用终端。这意味着你可以在程序运行的同时,继续在Shell中输入命令和与终端进行交互。

具体启动一个后台进程的方法是,在Shell中输入程序的命令,命令的末尾加上"&"符号,示例:

$ program_name &

当程序在后台运行时,Shell进程会立即返回一个进程ID(PID),然后就可以继续接收和执行其他命令。

而当我们按下"Ctrl+Z"组合键时,会向当前前台进程发送一个SIGTSTP信号,导致该进程被挂起。此时,终端的控制权会返回给Shell进程,并且挂起的进程会变为后台中断的进程。

通过"Ctrl+Z"将进程挂起后,可以使用"bg"命令将其切换到后台继续运行,或者使用"fg"命令将其切换到前台恢复执行,jobs查看后台进程组。

使用"bg"命令可以将挂起的进程设置为后台运行,示例:

$ bg %job_number

这里的"job_number"是挂起进程的作业号。

使用"fg"命令可以将挂起的进程设置为前台运行,示例:

$ fg %job_number

同样,"job_number"是挂起进程的作业号。

总结起来,后台进程中的程序不会占用终端。在Shell进程中启动一个程序时,可以通过在命令的末尾加上"&"符号来将其指定为后台进程。按下"Ctrl+Z"组合键会挂起当前的前台进程,并将控制权返回给Shell,使用"bg"命令可以将挂起的进程切换到后台运行,而使用"fg"命令则可以将其切换到前台继续执行。

八.守护进程

8.1概念

守护进程(Daemon)是在计算机操作系统中以后台形式运行的一类特殊进程。它们在系统启动时被启动,一直运行在后台,不依赖于任何用户交互,也不与任何终端相关联。

守护进程的主要目标是提供一种持续运行的服务,通常用于执行一些系统级任务或提供特定的功能和服务。例如,守护进程可以用于管理网络服务、定时任务、系统监控、日志记录等。它们通常作为系统的一部分,在操作系统启动时通过启动脚本或系统配置文件自动启动,并且在系统关闭前会被自动终止。

守护进程通常以父进程(通常是init进程)的方式运行,并且它们会脱离终端的控制。这意味着它们不会受到用户登录会话的影响,不会受用户注销或终端断开的影响,而且不会向终端输出任何信息。

此外,守护进程通常会将自己的运行日志写入日志文件,以便后续的故障排查和日志分析。它们在运行期间往往以系统权限运行,以便执行系统级任务,因此需要注意确保安全性。

总结来说,守护进程是在后台以系统服务的形式持续运行的特殊进程。它们在系统启动时自动启动,不依赖于用户交互和终端,主要用于执行系统级任务和提供特定的功能和服务。

8.2作用

守护进程(Daemon)在计算机系统中扮演着重要的角色,具有以下几个主要作用:

  1. 提供持续性服务:守护进程通常用于提供持续性的服务,这些服务在系统运行期间一直保持运行状态。通过守护进程,可以实现后台任务的自动化执行,例如服务的启动和停止,定时任务的执行,系统监控和维护等。

  2. 后台处理任务:守护进程通常用于执行一些需要在后台处理的任务。它们不依赖于用户交互和终端,可以在系统运行期间持久地进行工作。这主要包括一些系统级任务,如网络服务、文件处理、日志记录、备份、数据同步等。

  3. 提供服务和功能:守护进程用于提供特定的服务和功能。这些服务可以是网络相关的,如Web服务器、FTP服务器、邮件服务器等;也可以是系统级的,如定时任务调度器、系统监控服务、日志收集器等。通过守护进程,可以将这些功能以持续、自动化的方式提供给用户。

  4. 系统维护和管理:守护进程还用于系统的维护和管理。它们可以监视系统的运行状态,及时处理和报告问题。例如,守护进程可以监控硬件故障、内存和硬盘使用情况,提供警报和日志记录。同时,守护进程也可以协助系统管理员进行系统配置和管理,实现对系统资源的优化和控制。

总的来说,守护进程在计算机系统中承担着重要的角色,用于提供持续性服务、后台处理任务、提供特定的服务和功能,以及系统维护和管理。它们使得系统能够实现自动化、高效、稳定地执行各种任务,提供良好的用户体验和系统性能。

8.3特点

守护进程(Daemon)具有以下几个特点:

  1. 后台运行:守护进程以后台形式运行,不依赖于任何用户交互或终端。它们在系统启动时被自动启动,并在系统运行期间持续执行。这使得它们可以在系统运行期间提供服务和执行任务,而无需用户干预或终端的打开。

  2. 脱离终端:守护进程与任何终端无关,不会与终端进行交互。它们不会向终端输出任何信息,也不会接收来自终端的输入。这意味着守护进程在后台默默地执行任务,不会干扰用户的终端会话。

  3. 生命周期长:守护进程在系统启动时被创建,并持续运行直至系统关闭或手动终止。它们通常以父进程的方式运行,由系统的init进程或其他启动脚本完成启动。守护进程的长生命周期使得它们能够提供持续性的服务,处理长时间运行的任务。

  4. 不受用户会话影响:守护进程与用户会话无关,不受用户登录、注销或会话断开的影响。它们以系统级的身份运行,拥有独立的运行环境和权限。这使得守护进程能够在系统运行期间持续提供服务和执行任务,独立于特定用户或会话的状态。

  5. 日志记录:守护进程通常会将自己的运行日志写入特定的日志文件中。这些日志记录包含守护进程运行的详细信息,用于故障排查、性能分析和系统监控。日志记录可以帮助系统管理员了解守护进程的运行状况,发现问题并进行相应的处理。

总的来说,守护进程以后台形式运行,脱离终端,具有长生命周期,不受用户会话影响,同时也会进行日志记录。这些特点使得守护进程能够在系统运行期间提供持续性的服务,执行后台任务,并具备独立的运行环境和权限。

8.4编程步骤

  1. 创建子进程并终止父进程:使用 fork() 系统调用创建一个子进程,并在父进程中使用 exit() 函数终止,确保子进程可以继续执行。

  2. 在子进程中创建新会话和新进程组:使用 setsid() 系统调用创建一个新的会话,并使当前进程成为新会话的领导进程。这样可以脱离终端会话,避免受到终端信号的影响。

  3. 设置文件掩码:使用 umask() 系统调用设置文件掩码,通过屏蔽某些权限位来限制新建文件的默认权限。比如调用 umask(0) 将文件掩码设置为0,即不屏蔽任何权限位。

  4. 更改工作目录:使用 chdir() 系统调用将当前工作目录切换到一个合适的目录,一般选择一个不会被卸载的目录,例如根目录 /

  5. 关闭文件描述符:由于守护进程不再需要与终端交互,需要关闭标准输入、标准输出和标准错误输出的文件描述符。可以使用 close() 系统调用关闭这些文件描述符。

  6. 执行任务并处理信号:在守护进程中执行自己的任务逻辑,如网络服务或定时任务。同时需要注册信号处理函数,处理来自操作系统的信号,如重新加载配置或优雅地终止。

请注意,编写守护进程时除了上述步骤外,还应注意错误处理、日志记录和安全性等方面的问题。最好参考特定编程语言和操作系统的文档和示例代码以获取更详细的实现细节。

8.5编程实例

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

int main(int argc, char const *argv[])
{
    //1.创建子进程,父进程退出,子进程成为后台进程
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork is err");
        exit(-1);
    }
    else if (pid > 0)
    {
        exit(0);
    }

    //2.创建新会话,成为会话组组长,摆脱终端影响
    setsid();

    //3.改变当前工作目录
    chdir("/");

    //4.重新设置文件掩码
    umask(0);

    //5.关闭不需要的文件按描述符号
    for (int i = 0; i < 2; ++i)
        close(i);

    int fd = open("/home/hq/demo/进程/守护进程/temp", O_WRONLY | O_CREAT | O_TRUNC);
    if (fd < 0)
    {
        perror("open is err");
        exit(-1);
    }

    while (1)
    {
        write(fd,"hello\n",6);
        sleep(1);
    }
    return 0;
}

转载自:https://juejin.cn/post/7280746975917473847
评论
请登录