进程创建及守护进程

进程

进程的概念:

1、前提

智能手表:给用户提供了很多功能,比如心率实时检测、计步等功能,时间、消息、身体健康等数据展示,那么这里面涉及到不同功能之间的资源调用以及相互独立方面的问题。要求用户交互时手表能够及时反馈、如果心率功能不能使用,也不能影响到其他功能的正常使用,也就是说手表的程序编写涉及到功能之间的独立性、交互快速、资源的高效利用有很高的要求,而这些都离不开对进程和线程的理解和熟练运用

进程的好处:

  • 学会更高效的利用系统资源
  • 设计更加稳定和可靠的嵌入式程序
  • 提高程序的并发量
  • 提高程序的安全性
2、进程的概念

进程是操作系统中的一个基本概念。它是操作系统进行资源分配和调度的基本单位,是一个程序关于某数据集合上的一次运行活动,它是系统进行操作分派及资源分配的独立单位。

进程是操作系统分配资源和调度的一个独立单位。在技术上,它是一个执行中的程序的实例。具体来说,进程表示一个程序的执行过程,它包括程序代码及其所用数据的内存映像,系统资源,以及这个程序执行时的线程或线程集合。不同进程之间是相互独立的

理解:也就是说进程是一个运行中的程序,进程包含了运行这个程序所需要的一切资源,无论是加载的程序代码本身以及分配的内存资源或者是其他的东西,都是属于进程的一部分。

总结:

  • 进程是操作系统分配资源和调度的一个独立单位
  • 一个进程表示一个程序的执行过程
  • 不同进程之间是完全独立的

补充

进程和线程的区别对比

  1. 进程(Process)
  • 定义:进程是资源分配的最小单位。
  • 关键点:
    • 每个进程拥有独立的内存空间、文件描述符、系统资源等。
    • 进程间通信(IPC)需要通过专门的机制(如管道、消息队列、共享内存等)。
    • 进程是重量级的,创建和销毁的开销较大。
  1. 线程(Thread)
  • 定义:线程是CPU 调度的最小单位(也称为 “执行单元”)。
  • 关键点:
    • 同一进程内的多个线程共享进程的资源(如内存、文件句柄)。
    • 线程间通信更高效(直接访问共享内存),但需注意同步问题。
    • 线程是轻量级的,创建和切换的开销较小。
  1. 对比总结
对比维度 进程 线程
最小单位类型 资源分配 CPU 调度(执行)
资源占用 独立内存和系统资源 共享进程资源,仅拥有自己的栈和寄存器
通信方式 复杂(IPC) 简单(直接访问共享内存)
并发性 进程间并发 线程间并发(更轻量)
开销 高(创建 / 销毁 / 切换) 低(仅需保存 / 恢复上下文)
  1. 示例场景
  • 进程:打开一个浏览器窗口(一个进程),它拥有独立的内存空间和资源。
  • 线程:浏览器进程内的多个线程(如渲染线程、JS 执行线程、网络线程等),它们共享浏览器进程的资源,但独立执行不同任务。

总结

进程是系统进行资源分配和调度的基本单位,而线程是 CPU 调度和分派的基本单位。理解这一点有助于优化程序性能(如多线程并行计算)和资源管理(如避免进程间过度切换)。

3、进程的组成

进程由程序、数据集合和进程控制块三部分组成:

  • 程序:描述进程要完成的功能,是控制进程执行的指令集;
  • 数据集合:程序执行时所需要的数据和工作区,即内存空间;
    • 解释:进程占用的内存空间通常不是一整块连续的空间,而是分散的。在现代操作系统中**,进程的内存被组织为虚拟内存**。虚拟内存允许程序认为它拥有连续的内存空间,实际上这些空间可能被映射到物理内存的不同部分,甚至是硬盘上的交换文件空间
  • 程序控制块(PCB):包含进程的描述信息和控制信息,是进程存在的唯一标志。
    • 程序控制块(PCB,Process Control Block)是操作系统中用来存储关于进程信息的一种数据结构。它包含了操作系统需要管理和调度进程所必需的信息
      • 进程标识符(PID):一个唯一的标识,用来区分系统中的不同进程。linux 分配的进程的编号,每个进程都不一样方便管理。进程在结束时,会释放PID 的所有权。其他进程等待它释放一段时间后分配,但并不会结束后立马去分配出去
      • 进程状态:指示进程当前的状态,如就绪、运行、等待、停止等。
      • 程序计数器(PC):存储着该进程下一条要执行的指令的地址。
      • CPU寄存器信息:包括累加器、索引寄存器、栈指针等在进程执行过程中使用到的寄存器的当前值。
      • CPU调度信息:如进程优先级、调度队列指针、进程的其他调度参数。
      • 内存管理信息:如进程的地址空间、页表或者段表等内存管理相关的信息。
      • 会计信息:比如进程已使用的CPU时间量、实时时钟、时间限制、账号信息等。
      • I/O状态信息:包括分配给进程的I/O设备列表和打开文件列表等
4、进程的状态

(1)常见状态

  1. 新建状态(New):进程刚刚被创建,正在初始化,如分配PID、分配初始内存等。
  2. 就绪状态(Ready):进程已准备好运行并等待CPU分配时间。
  3. 运行状态(Running):进程正在使用CPU执行指令。
  4. 等待/阻塞状态(Waiting/Blocked):进程因为某种原因(例如等待I/O操作、等待资源)无法执行,被放入阻塞队列。
  5. 终止状态(Terminated):进程已完成执行或因错误退出,释放所占用的资源。

这些状态的转换是由操作系统内核的调度程序(scheduler)和其他机制控制和管理的。通过合理地管理进程状态,操作系统能够确保CPU资源得到高效地利用,同时也使得多个进程能够并发执行

(2)额外的进程状态

一些其他的操作系统(如Linux系统)还有额外的两个状态:

  • 挂起就绪状态(Suspended Ready):处于就绪状态的进程被挂起(换出内存),等待被重新激活到就绪状态。
  • 挂起阻塞状态(Suspended Blocked):处于阻塞状态的进程被挂起(换出内存),直到阻塞的原因消除,并且被重新激活到阻塞或就绪状态。

在这些系统中,操作系统通过挂起就绪和挂起阻塞状态,可以将不活跃的进程移出物理内存,暂时存储在磁盘上的交换区(swap space),从而为其他更高优先级或者活跃的进程释放宝贵的内存资源。当被挂起的进程需要恢复执行时,操作系统会将它们重新加载到内存中。这种机制提高了系统的灵活性和资源利用率

5、进程的生命周期

(1)一般情况下的进程生命周期

图:

进程创建及守护进程_第1张图片

生命周期的大概过程:

  • 创建:进程被创建,初始化其PCB,状态为新建。
  • 调度:进程被操作系统调度,转入就绪状态,等待CPU。
  • 执行:当CPU可用时,就绪状态的进程被调度为运行状态。
  • 中断/切换:
    • 如果运行中的进程发起I/O请求或在某些事件上等待,则转入等待(阻塞)状态。
    • 如果有更高优先级的进程需要运行或时间片用尽,运行中的进程可能被抢占,重新进入就绪状态。
  • 继续/重复:等待状态的进程在其等待的事件完成之后重新进入就绪状态;运行状态的进程在其时间片用尽或被抢占时重新进入就绪状态。
  • 结束:进程完成其任务后,或因错误或被操作系统终止,进入终止状态,并释放资源。

(2)一些状态的切换

  • 问:就绪状态是如何进入运行状态中

    • 处于就绪状态的进程被选中并分配CPU时间是通过操作系统内部的一个重要机制——CPU调度程序(CPU Scheduler)或调度器来实现的

      • 1.调度队列:
        • 系统将所有处于就绪状态的进程放入一个就绪队列(Ready Queue)中。
        • 该队列通常是根据特定的策略来组织进程,比如先来先服务(FCFS)、最短作业优先(SJF)等。
          • FCFS(先到,先服务):按照进程到达就绪队列的顺序进行调度。最早到达的进程将首先获得CPU时间,后到达的进程则等待前一个进程执行完毕
          • SJF(段任务优先):基于预估的下一个CPU突发的长度来选择下一个要执行的进程。具体来说,它选择那个预计会占用CPU时间最少的进程
          • 多级反馈队列 (Multilevel Feedback Queue, MFQ)
            • 抢占式调度算法。
            • cpu会维护多个进程队列,按照优先级划分,优先级高的会优先执行,但由固定的cpu时间(以微秒或纳秒为单位),执行后或执行过程中还有可能被更高优先级的待执行的进程被打断(比如vip被svip强行打断,打断的进程会重新进入就绪状态,即进入队列中,同时队列优先级不断变化,(比如长时间未运行的低优先级进程会提到高优先级的队列中)
      • 2.选择进程:
        • 调度程序根据所采用的调度算法从就绪队列中选择一个进程。
        • 例如,如果系统使用轮转调度(Round Robin)算法,它将依次为每个进程分配一个时间段(时间片)。
      • 3.分配CPU:
        • 选定的进程获得CPU控制权,其状态从“就绪”变为“运行”。
        • 在多核系统中,调度程序必须还决定将进程分配给哪个CPU核心,一般谁空闲就给谁。
      • 4.时间片:
        • 如果使用时间片或量子(quantum)的调度,进程将执行一段固定时间。
        • 一旦时间片结束,即使进程未完成,该进程也可能被中断并放回就绪队列。
      • 5.抢占:
        • 在抢占式调度中,当前运行的进程可能被另一个具有更高优先级的进程取代。
        • 当新进程到达或者一个进程的优先级发生变化时,调度程序可能会进行抢占。
      • 6.上下文切换:
        • 当执行进程切换时,会发生上下文切换,保存当前进程的状态,并加载下一个进程的上下文。

      以上步骤反复进行,确保每个就绪状态的进程都有机会获得CPU时间。调度算法的选择对于确保系统公平性和效率是非常关键的。在不同的操作系统和不同的场景中,可能会采用不同的调度策略以满足具体的系统性能要求

    (3)示例:执行一个c文件

#include 
#include 
#include 
#include 
#include  
#include 
#include 
//当前的父进程也是另外一个父进程的子进程,getpid是获取当前进程的pid,getppid是获取自己父进程的pid
//如果子进程中的父进程和原本父进程中的pid不同,是因为父进程先退出,子进程被领养,导致 getppid() 返回领养进程的 PID
int main() 
{
  	
  	// 创建子进程
  	pid_t pid = fork();
  	
  	if(pid > 0)
  	{
  		puts("----------父进程执行---------");
  		printf("这里是父进程里,执行的代码 :父进程的pid是:%d\n", getpid());
  		
  		// 父进程,等待子进程结束:
  		int status;
  		
  		pid_t temp_pid = wait( &status );
  		
  		printf("--- 父进程结束,子进程的结束码是:%d ,子进程的pid是:%d\n", status, temp_pid);
  		puts("-----------父进程结束------------");
  		
  	}
  	else if(pid == 0)
  	{
  		puts("===== 子进程执行 =====");
  		printf("这里是子进程里,执行的代码 :子进程的pid是:%d\n", getpid() );
  		printf("这里是子进程里,执行的代码 :父进程的pid是:%d\n", getppid() );
  		
  		sleep(1);
  		
  		puts("=== 子进程结束了 ===");
  		
  	
  	}
  	else 
  	{
  		perror("进程创建失败!");
  		exit(EXIT_FAILURE);
  	}
  
  	
  	printf("----------end:%d-----------\n", pid );
    return 0;
}
二、进程创建和父子进程
1、fork():创建一个子进程

(1)语法

#include  
#include 
pid_t fork(void);
  • 返回值:
    • 父进程中:返回-1,代表错误。新建子进程失败
    • 如果返回子进程的pid,代表新建子 进程成功 子进程中:返回0。
  • 注意
    • 失败的条件只有一个,内存不够了
    • 子进程一旦创建成功,就是一个独立的调度单位,和父进程是异步的。具体谁先被系统执行, 取决于系统的调度系统
2、父子进程

当我们使用fork函数时,就相当于创建了一个子进程,接下来我们来梳理父子进程之间的关系以及工作流程。

(1)fork进程的特点

当进程是通过fork进程创建之后,具有一下特点:

  • 创建的进程会有一份独立的内存空间,并同时会指定fork这句代码之后的所有代码,也就是和父进程一样执行一次剩余完整的所有的代码
    • 创建子进程,就相当于克隆了一份父进程。同时克隆的也有代码的执行状态
  • fork进程创建之后,父子进程谁先执行是受cpu调度管控,不明确谁先执行。可以通过判断执行fork函数的返回值来确定fork之后是谁先执行。如果fork函数的返回值>0那么是父进程,返回0的是子进程的PID,如果是-1,那么表示创建失败。如果==0,那么是子进程在执行代码,可以用分支结构来区分父子进程各自执行的代码
  • 子进程会继承父进程的程序计数器,CPU寄存器和打开文件的描述符。因此父子进程拥有相同的文本段、数据段、堆和用户态栈
  • 父子进程的PCB(进程控制块)不同,所以他们在进程队列中的位置也不同
  • 子进程的进程ID与父进程的不同,且大于父进程的进程ID
3、结束子进程

如果在程序的某个时候想要结束子进程,那么可以使用一下方式

  • 自然结束(main函数执行后,自然结束)
  • 使用exit()函数
  • 使用_exit()函数
  • 使用atexit函数

(1)exit函数

#include 
void exit(int status);
	功能: 结束一个进程,先释放缓冲区
参数:
 	status: 结束进程时的状态,
返回值:
	正常结束用0
	非正常结束-1

(2)_exit

include <unistd.h>
void _exit(int status);
	功能: 结束一个进程,不会释放缓冲区直接结束 (相当于暴力结束)
参数:
 	status: 结束进程时的状态,
return 
    正常结束用0 
    非正常结束-1

(3)atexit

include <stdlib.h>
int atexit(void (*function)(void));
	功能: 注册一个进程结束后的运行函数 // 当进程结束时会调用 function 这个函数
参数: 
	指向返回值时void类型参数时void 的一个函数指针
返回值:
 	成功: 返回 0
 	失败: 返回一个非 0
4、其他进程相关函数

(1)wait

  • 概念:阻塞等待子进程结束,然后回收子进程的资源

  • 语法

    #include 
    pid_t wait(int *status);
    
  • 参数

    • status:用于存储子进程的退出状态码。
  • 返回值

    • 成功: 终止子进程的pid
    • 错误: -1 errno做相应的设置
  • 特点

    • 在C语言中,pid_t是一种数据类型,通常用于存储进程ID(process ID)。在头文件里定义了pid_t作为进程ID的数据类型。其具体的实现细节(例如是长整型,整型还是短整型)可能因不同的系统或编译器而不同,但一般来说,它通常被定义为一个整数类型
  • 相关宏

    WIFEXITED(status):如果进程正常终止,返回真。这种情况下可以使用宏
    WEXITSTATUS(status)获取进程的退出状态码。
    WIFSIGNALED(status):如果进程是被信号打断,返回真。这时可以使用宏
    WTERMSIG(status)获取打断进程信号的编号。
    可以使用命令给进程发送信号:kill -信号编号 进程的pid
    

(2)waitpid

  • 概念: 阻塞等待指定子进程结束,然后回收子进程的资源

  • 语法

    #include 
    pid_t waitpid(pid_t pid, int *status, int options);
    
  • 参数

    • pid: 指定要回收的子进程的pid
    • status:用于存储子进程的退出状态码。
    • options: 0 阻塞等待子进程的结束。 WNOHANG 非阻塞
  • 返回值

    • 成功: 终止子进程的pid 。如果 options的值为WNOHANG,要回收的子进程还没 有结束,返回0。
    • 错误: -1 errno做相应的设置 。

(3)getpid

  • 概念:获取当前进程的PID

  • 语法

    #include 
    pid_t getpid();
    
  • 返回值

    • 成功:返回当前进程的pid

(4)getppid

  • 概念:获取父进程的pid

  • 语法

    #include 
    pid_t getppid();
    
  • 返回值

三、特殊进程
1、孤儿进程

(1)概念

是指一种特殊状态下的子进程。该子进程的父进程已经结束,但子进程仍未结束,那么这样的子进程就是孤儿进程,即没有老爸了。

(2)特点

但按照Unix和Linux的规定,孤儿进程的父进程将被设置为init进程,也就是PID为1的进程。也就是说当一个子进程变成孤儿进程后,会强行将pid为1的进程作为子进程的老爸,也就是继父

注意:init进程是在系统启动时创建的,其负责真正地删除所有的孤儿进程。当一个孤儿进程的父进程终止时,其的父进程会自动设置为init。当孤儿进程结束时,init进程会处理孤儿进程的退出状态(return code),从而确保系统资源的正确释放。也就是说孤儿进程本身只是状态比较特殊,本身对系统没有什么危害

2、僵尸进程

(1)概念

是指这样的子进程,子进程已经结束,但父进程一直忙于自己的事没有清理该子进程的资源,导致子进程的资源(比如pcb)没有被释放。那么这样的子进程被称为僵尸进程,似死未死

父进程回收子进程:简单来说,进程的生命周期是这样的:创建 -> 运行 -> 结束。当一个进程结束时,它的进程描述符(包括进程状态、退出码等信息)仍然会留在系统中,这就需要父进程来读取这些信息并完成清理工作,这个过程称为“回收”子进程。

(2)僵尸进程危害

如果父进程没有回收子进程,那么子进程虽然已经结束,但是其进程描述符仍然在,它就变成了僵尸进程。每个僵尸进程在内核中占用一定的资源,如果大量僵尸进程存在,可能会消耗尽系统的进程资源。

子进程在执行结束后大部分资源已经被释放,但是如进程ID、结束状态(也就是其进程控制块PCB中的一部分信息)等还会保留在系统中,需要父进程来回收。一旦父进程完成了这个回收工作,那么这个进程的所有资源就都被清除,它在系统中就不存在了。但是如果父进程没有处理的话,就会导致僵尸进程的产生,一小部分还好,但是如果有大量僵尸进程,还是会产生一定影响。

(3)释放僵尸进程

通常,可以通过编程方式避免僵尸进程的产生,比如在父进程中通过调用wait()waitpid()函数等待子进程结束,并回收其资源.或者也可以在父进程执行完后结束父进程,那么僵尸进程会变成孤儿进程,从而被init进程释放掉。但推荐在代码中也保证子进程正常的结束。

3、守护进程

(1)概念

守护进程是一种在后台运行的特殊进程(周期性的进程),它独立于控制终端并周期性地执行某种任务或者等待处理某些发生的事件。守护进程通常在系统引导时启动,并在系统关闭时终止。它们通常用于执行一些系统级的任务,比如邮件服务器、日志处理、定时任务等。

Linux系统在启动时就已经创建了很多守护进程。比如systemdNetworkManagersshd等,以下是其中几种守护进程的说明

  • systemd:这是Linux系统中最先运行的进程,其主要任务是启动其他系统进程。它具有强大的并发性能,允许同时启动多个守护进程,加快系统启动速度。此外,systemd还负责各种系统维护任务,如挂载文件系统,设置网络,启动和停止服务等
  • NetworkManager 是一个动态网络配置守护进程,其主要任务是简化网络设备和连接设置,自动切换无线连接等。除此以外,它还提供了一套命令行界面和图形界面,方便用户进行网络管理
  • sshd 是 OpenSSH 服务的守护进程,主要用于管理网络上的远程访问。通过ssh协议,用户可以远程登录到系统,执行命令或者传输文件等。

(2)作用

  • 后台服务:守护进程最常见的功能就是提供后台服务,例如web服务器、数据库服务器、文件服务器等,他们都是以守护进程形式运行,接收和处理来自用户或其他程序的请求。

  • 定时任务:某些守护进程用于定时执行某些任务,例如cron守护进程可以周期性的执行预定义的命令或脚本。

  • 事件响应:有些守护进程用于监听系统或硬件的某些事件,当这些事件发生时执行相应的操作。例如,ACPID守护进程在接收到电源状态改变的消息时会做出相应的响应。

  • 资源管理:守护进程也常常用于系统资源管理,例如系统日志守护进程rsyslogd负责系统日志的收集、筛选、转发和保存,网络管理守护进程NetworkManager负责网络设备的管理和网络连接的自动配置。

  • 硬件交互:有些守护进程用于支持和管理与硬件设备的交互,例如打印机守护进程CUPS,可帮助用户设置打印任务,处理与打印机的交互。

    总结:能够在后台周期执行任务,给系统不同的功能模块提供持续的服务。比如定时清理、日志备份等

(3)创建守护进程

  • 步骤

    • 1.创建子进程,父进程退出(目的:让子进程变成孤儿进程)
    • 2.子进程创建新会话,并成为会话首进程(让该进程彻底和之前的进程组断绝关系,成为独立的进程,其中的动作也不会影响到其他进程,同时也会保证哪怕终端关闭之后也不会导致守护机进程收到影响,这就使得它可以在后台长期运行,而不用担心用户是否还在登录,是否还在运行原进程)
    • 3.改变工作目录为根目录,重新定位标准输入和输出以及错误(避免会向屏幕上有输出)
    • 4.在一个无限循环中执行守护进程的任务代码
    • 可选:如果要结束守护进程,那么在守护进程初始化工作完成后输出或记录其PID,然后在某个终端用kill命令来结束守护进程。
      • 比如kill -9 1276kill 1276。其中1276是进程PID,-9表示发送的sigkill信号。SIGKILL是Unix和类Unix系统中的一个信号,它的值通常为9。此信号发送给一个进程后,会立即终止该进程(有可能进程资源释放会异常)。如果没有-9,那么表示发送的SIGKILL信号值为15,即SIGTERM,也是默认值。会更优雅 的结束进程(会先释放进程所占资源,然后再结束)。即-9:暴力结束 。 -15 或不写:优雅结束
  • 创建守护进程代码示例:

#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 


void createKeep()
{
	 	// 创建子进程
  	pid_t pid = fork();
  	
  	if(pid > 0)
    {
  		exit(0);
  	}
    else if(pid == 0)
    {
  		// 创建守护进程,只需要子进程:
  		pid_t sid = setsid(); // 创建新会话,并使调用进程成为该会话的领头进程。这也会使进程脱离任何控制终端。
  		if(sid == -1)
        {
  			perror("set sid error!");
  			exit(1);
  		}
  		// 这个pid就是守护进程的id
  		pid_t keep_id = getpid();
  		printf("守护进程的pid是:%d\n", keep_id );
  		
  		// 切换工作目录 :
  		if(chdir("/") < 0)
        {
  			perror("change dir error!");
  			exit(1);
  		}
  		// 最后关闭文件描述符
  		close(STDIN_FILENO);
  		close(STDERR_FILENO);
  		
  	
  	}else 
    {
  		perror("进程创建失败!");
  		exit(EXIT_FAILURE);
  	}
}

int main() {
	puts("==== 守护进程创建中 ====");
  	
 	createKeep();
  	
  	while(true)
    {
		puts("守护进程执行中......");
		
		sleep(3);  	
  	}
  
    return 0;
}
  • 通过查看进程id来看守护进程是否存在:

    ps aux
    

守护进程的特点和作用

一、特点
  • 背景运行:独立于终端,不受用户登录 / 注销影响(如 Linux 的systemd服务)。
  • 生命周期长:随系统启动而启动,持续运行直至手动停止(如日志服务、网络服务)。
  • 资源隔离:与终端断开连接,不占用控制终端(stdin/stdout/stderr通常重定向到/dev/null)。
二、作用
  • 持续提供服务:无需用户交互,后台执行固定任务(如 Web 服务器nginx、数据库mysql)。
  • 系统稳定性:处理底层服务(如硬件监控、定时备份),避免前台程序干扰。
  • 资源高效利用:低功耗运行,不占用终端界面或用户交互资源。
三、典型例子
  • Linux 系统sshd(远程登录服务)、crond(定时任务服务)。
  • 日常场景:手机后台的 “应用更新服务”“消息推送服务” 本质类似守护进程
  • 智能家居设备:路由器的 WiFi 管理服务、智能摄像头的视频流处理服务。
  • 防火墙管理服务(firewalld/ufw):后台运行并执行网络流量过滤规则(如阻止恶意 IP 访问)。
  • 病毒扫描服务(ClamAV):定期扫描系统文件,实时监控病毒入侵(如服务器防病毒)。
  • 系统日志服务(rsyslogd/systemd-journald):收集并存储系统和应用日志(如记录程序崩溃信息)。

你可能感兴趣的:(物联网,c语言)