当前位置: 首页 > news >正文

进程启动和进程终止

文章目录

  • 进程
    • 程序
    • 进程
    • 进程ID
    • 进程表项
    • C程序的启动过程
      • 启动例程
      • 进程终止
      • atexit函数
        • 示例--终止函数的执行流程以及多种进程终止方式的对比
      • 进程启动和退出流程图
      • 查看系统中的进程

进程

程序

  • 程序是存放在磁盘文件中的可执行文件。当代码进行编辑保存后使用gcc等编译工具进行编译链接最后生成的可执行文件就是程序。

    image-20240827111700110

进程

  • 程序的运行实例叫做进程(当程序运行起来就变成了进程)

    image-20240827111927095

    可以使用指令ps来查看此时正在运行的hello进程

    ps -elf | grep hello | grep -v grep #ps -elf: 列出系统中所有进程的详细信息。其中,-e表示显示所有进程,-l表示长格式输出,-f表示显示完整格式。
    #grep hello: 从上一步的输出中筛选出包含"hello"字符串的行。
    #grep -v grep: 从第二步的输出中排除包含"grep"字符串的行,以避免将grep命令本身也作为结果输出。
    

    image-20240827112118858

  • 进程具有独立的权限和职责。如果系统中的某个进程崩溃,它不会影响到其余的进程。这是因为系统会给每一个进程分配一个虚拟内存,当一个进程崩溃并不会影响到别的进程的内存空间。

  • 每个进程运行在其各自的虚拟地址空间中,进程之间可以通过由内核控制的机制相互通讯(进程间通信IPC)

进程ID

  • 每个Linux进程都有一个唯一的数字标识符,称为进程ID,进程ID一定是非负整数。

    在Linux中可以使用指令ps指令来查看此时后台的所有进程

    ps -elf | more#ps 是一个用于报告当前系统进程状态的命令。
    #-elf 是 ps 命令的选项组合,其中:
    #-e 表示显示所有进程,包括其他用户的进程。
    #-l 表示使用长格式输出,显示更详细的信息。
    #-f 表示显示完整格式,包括进程的UID、PID、PPID、C、STIME、TTY、TIME、CMD等详细信息。
    # | 是管道符,用于将前一个命令的输出作为后一个命令的输入。
    # more 是一个用于分页显示文本内容的命令,当输出内容较多时,可以使用它来逐页查看。
    

    image-20240827113548989

    这里的PID就是进程的标识符,而PPID指的就是父进程的进程标识符,即创建这个进程的进程。进程标识符为1的是init进程,它是由0号进程idleswapper创建的。0号进程在系统启动时由内核自动创建,它是唯一一个没有通过fork()kernel_thread()产生的进程。所以说0号进程是所有进程的祖先。

进程表项

当一个程序运行起来后,系统会给它分配一片虚拟内存空间,这片内存空间可能远远大于实际的物理内存。在虚拟内存中,用户空间占用大部分内存,这片空间存放着进程的代码段、数据段和堆栈段等等;还有一部分是内核空间,当一个进程运行的时候会在内核中生成一个进程表项用来存储这个进程的信息。例如进程操作的文件的信息、信号、还有和物理内存映射等相关信息。

在Linux系统中可以查询这个进程表项的相关信息:

首先跳转到/usr/src/linux-headers-6.5.0-41-generic/include/linux这个目录下,然后使用grep "struct task_struct {" * -nir查找在哪里出现了这个字段,然后打开对应的头文件查看

image-20240828212926477

有关内存的结构体:image-20240828214453838

进程ID:image-20240828214545884

有关信号的结构体:image-20240828214802522

C程序的启动过程

前边讲过C程序编程可执行程序需要经过:预处理、编译、汇编、链接四个步骤,在这之后就会生成一个可执行文件C程序的启动过程。当去运行可执行文件的时候,总是从主函数开始。其实在主函数之前内核还会启动一个特殊例程就是启动例程

启动例程

启动例程在系统中其实已经编译好了,通常放在/lib/lib32或/lib/lib64目录下。编译器在编译时会将启动例程编译到可执行文件中去。

启动例程的作用

  • 搜集命令行的参数传递给main函数中的argcargv

    在命令行中通常会输入以空格隔开的若干个字符串,启动例程将这些字符串的个数和内容传给main函数中的argcargv

  • 搜集环境信息构建环境表并传递给main函数

    将环境变量$PATH等信息构建成一个环境表传给main函数

  • 登记进程的终止函数

    在每个进程结束之前都会去执行一个默认的终止函数,例如C程序在return之前就会去执行一个终止函数。终止函数主要是负责资源的释放,例如文件描述符的关闭、缓存的强制清空等。系统默认有一个终止函数,但是用户也同样能够自己编写一个终止函数,然后向内核去注册这个终止函数,在程序结束之前由用户去自定义释放哪些资源。

进程终止

  • 正常终止

    • 从main函数中返回 (return 0)
    • 调用标准C库函数 (exit(0))
    • 调用系统调用函数_exit_Exit
    • 最后一个线程从启动例程中返回
    • 最后一个线程调用pthread_exit(最后一个线程调用这个意味着所有的工作执行完毕)
  • 异常终止

    • 调用abort

      在C语言中有一个断言函数assert,它的原理就是当不满足条件时,通过调用abort()函数来终止程序的运行,实际上abort函数会向自身发送SIGABRT信号,触发默认的SIGABRT信号处理程序

    • 接收到一个信号并终止

      在实际开发中经常会遇到死循环的情况,这时候需要按下Ctrl+CCtrl+Z来结束程序的运行,实际上这两个操作是发送了一个信号来终止程序的运行。分别是SIGINTSIGSTP信号。

    • 最后一个线程对取消请求做处理响应

  • 进程返回

    • 通常程序运行成功返回0,否则返回非0;

      在实际开发中,如果成功执行的话,return的返回值是0反之是非0,exit系统调用函数的传参也是一样。

    • shell中可以查看进程的返回值:echo $?

默认每个进程终止的时候都会去调用进程终止函数,实际上可以用户可以自己定义终止函数,由用户自己去指定要释放的资源,然后通过向内核注册用户自己编写的函数就可以了。然后并不是每一种终止方式都会去调用终止函数,下边用具体的实例来演示哪些终止方式在调用的时候会去调用终止函数。

atexit函数

#include <stdlib.h>int atexit(void (*function)(void));//功能:允许用户注册一个函数,在main函数终止时自动被调用
//参数:一个函数指针即函数名
//返回值:如果成功执行返回0,否则返回非0

注意事项

  • 可以使用多次atexit函数来注册多个退出处理函数,这些函数按照它们被注册的相反顺序被调用。即最后注册的函数将先被执行(执行顺序原理类似于栈)
  • 如果atexit函数注册失败,它会返回非零值。这种情况一般发生在注册了太多的退出处理函数
示例–终止函数的执行流程以及多种进程终止方式的对比
#include "header.h"void exit_handler1(void)
{printf("first term func1\n");
}void exit_handler2(void)
{printf("second term func2\n");
}void exit_handler3(void)
{printf("third term func3\n");
}int main(int argc, char **argv)
{if(argc < 3){fprintf(stderr,"usage: %s [filepath] [exit_type:exit|_exit|return]\n",argv[0]);exit(EXIT_FAILURE);}//向内核登记终止函数if(atexit(exit_handler1) != 0){perror("atexit");exit(EXIT_FAILURE);}if(atexit(exit_handler2) != 0){perror("atexit");exit(EXIT_FAILURE);}if(atexit(exit_handler3) != 0){perror("atexit");exit(EXIT_FAILURE);}FILE *fp = fopen(argv[1],"w");if(fp == NULL){perror("fopen");exit(EXIT_FAILURE);}fprintf(fp, "hello world");if(!strcmp(argv[2],"return")){return 0;}else if(!strcmp(argv[2],"_exit")){_exit(EXIT_SUCCESS);}else if(!strcmp(argv[2],"exit")){exit(EXIT_FAILURE);}else{fprintf(stderr,"usage: %s [filepath] [exit_type:exit|_exit|return]\n",argv[0]);exit(EXIT_FAILURE);}
}

image-20240903104328682image-20240903104430466image-20240903104514721

通过执行结果可以发现,在通过exitreturn作为进程的终止方式时,程序中向内核登记的终止函数都会被执行。而且它们的执行顺序以栈的方式进行执行,先登记的后执行,后登记的先执行。并且也成功地创建了文件,并向文件中写入了数据。而_exit这个函数在它是一个系统调用函数,这里调用_exit函数并没有去调用进程终止函数,并且也没有将缓存里的数据清空,写入到文件里。这一点需要注意,以后在使用进程终止方式的时候要选择恰当的终止方式,要不然它的结果可能和预期的结果不一样。

在代码中,向文件写入数据的方式属于全缓存,全缓存将数据写入到文件里有三种情况:1.当全缓存满的时候会将数据全部写入到文件里;2.当使用fflush强制清空缓存也会将数据写入到文件里;3.当使用fclose函数关闭文件的时候也会将缓存里的内容写入到文件里。(具体有关缓存的知识可以查看:标准C的IO缓存类型)这里通过exitreturn作为进程的终止方式时,标准I/O库会检查所有打开的文件描述符,并自动将缓存区的数据写入到文件中去。也就是说fprintf函数并不是直接将数据写入到文件里,而是先写入到缓存里,最后当使用fclose函数关闭文件指针的时候将缓存的内容写入到文件里。而当选择_exit作为进程的终止方式时,由于_exit函数是一个底层的系统调用,它直接终止进程而不会执行一些高级的终止处理,包括调用注册的终止处理函数和清空缓存写入文件。

进程启动和退出流程图

image-20240903112212674

如图所示:介绍一下进程的启动和退出的流程

  1. 首先在main函数调用之前,内核会调用一个启动例程,也就是这里的C start template来负责将从命令行传入的参数的个数和参数的内容进行收集然后传给main函数中的argc argv;然后将环境变量$PATH $SHELL等构建成一个环境表,这个也会传给main函数;然后会向内核登记终止函数,例如程序里的atexit登记的三个进程终止函数。最后它会去调用main函数。
  2. main函数又会去调用其他的函数,在这个过程中,main函数和其他被调用的函数可以调用系统调用函数_exit或_Exit来终止进程的运行,由于_exit_Exit都是系统调用,所以它并不会执行一些高级的处理。
  3. 如果main函数和其他被调用的函数执行了exit标准C库函数,那么它会执行一些高级操作例如以栈的运行方式去调用之前向内核登记的终止函数和清空缓存将数据写入到文件等操作。exit函数在底层仍然是通过调用_exit或_Exit来实现进程的退出。

查看系统中的进程

使用ps指令可以查看当前系统中的一些进程的信息,后边加不同的参数选项能够显示不同的信息。

使用ps -ef | more来查看当前系统中的信息

image-20240903153448537

这里的UID指的是当前运行这个进程的用户的名字,例如这里的root超级用户或者后边的普通用户;这里的PID是进程的唯一标识符,简称进程编号;PPID指的是它的父进程;STIME指的是它启动的时间;TTY指的是它运行的终端;TIME指的是运行它这个进程所花费的时间;CMD指的是此进程运行的指令。

使用ps -aux|more来查看进程占据的资源信息

image-20240903154219924

这里的USER就是进程的属主,也就是运行这个的用户;%CPU就是它占据CPU的百分比,%MEM是占据内存的百分比,STAT就是当前进程的状态信息。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Python 中 Locale.Error: Unsupported Locale Setting 错误
  • JAVA基础:线程优先级和精灵线程
  • CGAL GIS 应用 - 从点云到DTM
  • 勇于尝试,永远行动 - 《洛克菲勒写给儿子的38封信》读书笔记
  • 计算机毕业设计 扶贫助农系统的设计与实现 Java实战项目 附源码+文档+视频讲解
  • 题目:单调栈
  • Java自学之路:掌握接口的艺术
  • fpga系列 HDL:全连接层的浮点数乘法器FM实现
  • maya的重命名物体和材质工具(带ai过程)
  • 机器学习 vs 深度学习:深入浅出解析两者的区别
  • 【Java基础】String详解
  • overleaf如何引用文献
  • 时序预测 | Matlab实现SSA-TCN麻雀搜索算法优化时间卷积网络时序预测-递归预测未来数据(单输入单输出)
  • 【每日刷题】Day123
  • Java 21的Enhanced Deprecation的笔记
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • 【知识碎片】第三方登录弹窗效果
  • Babel配置的不完全指南
  • ES6 ...操作符
  • java8-模拟hadoop
  • JavaScript标准库系列——Math对象和Date对象(二)
  • python 装饰器(一)
  • spark本地环境的搭建到运行第一个spark程序
  • 观察者模式实现非直接耦合
  • 记一次和乔布斯合作最难忘的经历
  • 前端相关框架总和
  • 什么是Javascript函数节流?
  • 使用 Xcode 的 Target 区分开发和生产环境
  • ​探讨元宇宙和VR虚拟现实之间的区别​
  • ## 基础知识
  • #define 用法
  • #laravel部署安装报错loadFactoriesFrom是undefined method #
  • (02)vite环境变量配置
  • (2)STM32单片机上位机
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (C++哈希表01)
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第2节(共同的基类)
  • (SERIES12)DM性能优化
  • (TOJ2804)Even? Odd?
  • (不用互三)AI绘画工具应该如何选择
  • (含笔试题)深度解析数据在内存中的存储
  • (三)SvelteKit教程:layout 文件
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (转)微软牛津计划介绍——屌爆了的自然数据处理解决方案(人脸/语音识别,计算机视觉与语言理解)...
  • *setTimeout实现text输入在用户停顿时才调用事件!*
  • .Mobi域名介绍
  • .NET Core 2.1路线图
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET IoC 容器(三)Autofac
  • .net6解除文件上传限制。Multipart body length limit 16384 exceeded
  • ::
  • @AutoConfigurationPackage的使用
  • @converter 只能用mysql吗_python-MySQLConverter对象没有mysql-connector属性’...
  • @Tag和@Operation标签失效问题。SpringDoc 2.2.0(OpenApi 3)和Spring Boot 3.1.1集成