Linux进程概念及状态

news2024/11/20 4:52:18

Linux进程概念及进程状态

目录

  • Linux进程概念及进程状态
    • 引入
    • 1、什么是进程
      • 1.1 描述进程
      • 1.2 task_struct组织进程
      • 1.3 proc目录
    • 2、进程标识符
    • 3、查看进程
    • 4、bash进程
    • 5、初始fork
    • 6、进程状态
      • 6.1 操作系统层面
      • 6.2 Linux内核源代码
        • R
        • S
        • D
        • X
      • 6.3 僵尸进程
      • 6.4 孤儿进程
        • 关于kill指令

引入

在计算机科学中,进程是一个正在执行中的程序的实例。它是操作系统进行资源分配和调度的基本单位。Linux作为一种流行的操作系统,也采用了进程的概念来管理系统中的任务和应用程序。

在本篇博客中,我们将深入探讨Linux进程的概念以及进程状态。我们将解释进程的定义、进程的生命周期、以及进程状态的转换过程。此外,我们还将介绍一些常用的Linux命令,以便您可以更好地了解和管理Linux系统中的进程。

1、什么是进程

进程是计算机中正在运行的程序实例

👉 进程 vs 程序

程序的本质是存放在硬盘上的文件,而进程是一个运行起来(加载到内存中)的程序,因此进程具有动态属性

1.1 描述进程

管理的本质是先描述,再组织;在操作系统的进程管理上同样如此,我们将进程的各个属性先描述,再利用某种数据结构讲它们组织起来,这样就可以很好的将进程管理起来

在Linux系统中,使用struct来进行对进程的描述,该结构体称为PCB(进程控制块)

PCB通常包含以下信息:

  1. 进程标识符:每个进程都有一个唯一的标识符,用于区分不同的进程。
  2. 进程状态:进程可以处于就绪、运行或者阻塞状态。PCB中记录了当前进程的状态,以及进程在不同状态间的转换条件。
  3. 程序计数器(PC):PCB中记录了程序计数器的值,即下一条要执行的指令的地址。
  4. 内存指针:记录了进程占用的内存空间的起始地址和结束地址。
  5. CPU寄存器:记录了进程在执行过程中需要保存的CPU寄存器的值,例如程序计数器、堆栈指针等。
  6. 进程优先级:记录了进程的优先级,用于操作系统调度程序决定哪个进程先执行。
  7. 执行时间:记录了进程已经执行的时间和剩余的执行时间。
  8. I/O状态:记录了进程在进行I/O操作时的状态,以及需要进行I/O操作的设备和资源。

此时,所谓的对进程进行管理,变成了对进程对应的PCB进行相关的管理!

对进程管理:转化成了对链表(某种数据结构)的增删查!

1.2 task_struct组织进程

Linux操作系统下的PCB是: task_struct

image-20230411154621011

struct task_struct 内核结构体 -> 内核对象task_struct —> 将该结构与对应代码和数据关联起来 

前文介绍过,进程的是硬盘中的程序加载到内存中,每个加载入内存的可执行程序,即对应一个描述它们属性的struct task_struct,如图:

image-20230411160714917

1.3 proc目录

进程信息保存在根目录下的proc目录中

image-20230411161023288

proc是Linux系统上的内存文件系统,在proc当中存储着当前系统实时的进程信息:

ls proc

image-20230411161357508

2、进程标识符

每一个进程在系统中,都会存在一个唯一的标识符,用来标识唯一的一个进程,也叫做pidprocess id)。

进程id:PID 父进程id:PPID

我们可以调用以下接口来查看进程id:

getpid();    //得到进程id
getppid();   //得到父进程id

我们可以通过man指令查看linux手册关于此接口的介绍:

man getpid

image-20230411162851502pid_t代表无符号整型

下面我们执行以下程序:

vim Test.c
//Test.c
#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>                             
    
int main()    
{    
    while(1)    
    {    
        printf("子进程PID:%d,父进程PPID:%d\n",getpid(),getppid());        
        sleep(1);    
    }    
    
    return 0;    
}
gcc Test.c -o Test.exe
./Test.exe

运行结果如下:

image-20230411163217659

我们由以上信息可以得到该进程的pid,我们执行:

ls /proc/551/       #其中551是子进程pid

image-20230411163519223

我们发现,当我们运行Test.exe程序时,获取进程的pid,再查看proc目录下的pid文件夹,发现一定会存在一个以该程序pid命名的文件夹;

当我们使用Ctrl C结束该程序时:

image-20230411163738813

此时,该进程文件夹消失了!

刚刚我们采用Ctrl C关闭正在执行的程序,我们也可以使用kill指令结束进程,命令如下:

kill -9 [进程的pid]

image-20230411164128536

3、查看进程

大多数进程信息可以使用ps用户级工具来获取

例如我们要查看Test.exe程序的进程信息:

ps ajx | grep Test.exe

image-20230411165410416

使用上述grep命令后我们发现屏幕上会显示有两个进程信息,这是因为grep指令也是一个进程,可以通过如下指令去掉grep进程信息:

//-v表示匹配上的不显示
ps ajx | grep Test.exe | grep -v grep

而我们发现进程信息为我们显示了该进程的各个属性,但它们名没有名称,我们可以加上:

//显示各项属性名称,且不显示grep的进程
ps ajx | head -1 && ps ajx | grep Test.exe | grep -v grep 

image-20230411165633103

通过该指令可以得到该进程的详细信息,我们最常用的即是它的PIDPPID

4、bash进程

我们试着多次运行Test.c程序:

我们发现:子进程pid一直在变化,而父进程pid却一直没有变化!

那么,这个pid为3743的父进程,是什么呢?我们查看它的进程信息:

ps ajx | head -1 && ps ajx | grep 3743 | grep -v grep

image-20230411170339223

结论:几乎所有我们在命令行上所执行的指令,都是bash进程的子进程!

5、初始fork

man fork

image-20230411170601996

fork() creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process, referred to as the parent

fork()系统调用通过复制调用进程来创建一个新进程。新进程被称为子进程,是调用进程的一个完全副本,也被称为父进程。

其返回值如下:

image-20230411171112150

fork有两个返回值,子进程中fork返回0,父进程中fork返回子进程的pid。

示例:我们通过vim创建如下文件:

//fork_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
    }
    else if(id > 0)
    {
        //父进程
        while(1)
        {
            printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(3);                                                                          
        }
    }
    return 0;
}
gcc fork_test.c -o fork.exe
./fork.exe

运行结果如下:

image-20230411193400631

程序中两个死循环同时运行,查看此时的fork进程信息:

ps ajx | head -1 && ps ajx | grep fork.exe | grep -v grep

image-20230411193721075

我们发现有两个fork进程,并且这两个进程是父子进程的关系(第一个进程是另一个进程的父进程)

  • 父进程可以有多个进程,但子进程只能有一个父进程;
  • 而父进程可能有多个子进程,因此需要pid来标识每一个子进程;
  • 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本很低。

这真的是太奇怪了!函数返回值只有其中一个!分支语句也只能选择一个!

所以我们调用完fork()后,操作系统究竟做了什么?fork之后系统多了一个进程,实质就是内存中多了一个task_struct结构体以及子进程对应的代码和数据;子进程的task_struct对象内部的数据基本是从父进程继承下来的,代码和数据则是fork之后父子进程代码共享,数据各自独立

创建子进程–fork是一个函数–函数执行前:只有一个父进程–函数执行后:父进程+子进程

6、进程状态

6.1 操作系统层面

进程状态有:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡等等进程状态,进程具有如此多的状态,本质是满足未来不同的运行场景的;要理解进程状态,我们首先需要搭建一个os系统的宏观概念:

image-20230414131409064

结论:

1、一个CPU一个运行队列(runqueue)

2、让进程进入队列,本质是将该进程task_struct结构体对象放在运行队列中!

3、进程PCB在runqueue中,就是运行状态(R),不是说这个进程正在运行,才是运行状态!

4、进程不仅仅只等待(占用)CPU资源,也随时随地需要外设资源

5、所谓的进程状态不同,本质是进程在不同的队列中,等待某种资源!


image-20230414135038559


因此我们可以总结出,在操作系统层面上,三种重要的进程状态:

运行状态:进程只要在(CPU)运行队列中,就是运行状态

阻塞状态:当进制等待某种非CPU类资源时,该资源还未就绪,进程PCB在该资源等待队列中,即为阻塞状态

挂起状态:当内存不足时,操作系统会将短期内不会调度执行的进程的代码和数据从内存中替换出去


6.2 Linux内核源代码

有了上述在操作系统层面上,对于进程状态的认识,我们来认识Linux操作系统具体的状态,状态在LInux内核源代码中定义如下:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */     	//运行状态
"S (sleeping)", /* 1 */   	//阻塞状态(浅度睡眠)
"D (disk sleep)", /* 2 */   //一种阻塞状态(深度睡眠)
"T (stopped)", /* 4 */		//暂停状态
"t (tracing stop)", /* 8 */ //暂停状态
"X (dead)", /* 16 */		//死亡状态
"Z (zombie)", /* 32 */		//僵尸状态
};

在Linux内核当中,进程状态可以理解为就是一个整数,例如:

//Linux中的进程状态在task_struct
//以下仅是示例
#define RUN 1   	//用1表示运行  
#define STOP 2		//用2表示停止  
#define SLEEP 3		//用3表示睡眠  

具体状态介绍如下:

R

R代表运行状态,进程在运行队列中

S

S阻塞状态,代表进程在等待某种资源,该进程的PCB在其等待资源的等待队列中

由于CPU的运行速度很快,而资源(硬件外设)的速度很慢,因此一个程序从加载到内存中开始运行,大部分时间都是在等待外设资源,因此大部分时间处于阻塞状态

例如,当我们涉及printf输出时即需要访问外设资源,此时我们查看该进程状态如下:

ps ajx | head -1 && ps ajx | grep Test.exe | grep -v grep

image-20230414143531204

D

S 是一种阻塞状态,是浅度睡眠,可中断睡眠,操作系统和用户都可以中断其睡眠。

D 是磁盘休眠状态(Disk sleep),也一种阻塞状态,有时候也叫不可中断睡眠状态(uninterruptible sleep),在该状态的进程,无法被OS杀掉!只能通过断电或者进程自己醒来解决

该状态出现的原因?

当计算机处理大量工作时(高IO的情况下),由于内存大小时固定的,当我们正在对某些重要信息处理时,为了避免因为内存不足,操作系统终止一些用户进程导致信息丢失的情况,Linux中提高了深度睡眠状态,使得该状态的进程是无法被操作系统和用户终止,通过这种方式保护我们正在处理的重要信息

X

进程进入死亡状态,资源可以立马被回收。这个状态只是一个返回状态,你不会在任务列表里看到这个状态

6.3 僵尸进程

当进程被创建出来,其目的是为了完成某个任务,那我们怎么知道它是否完成了呢?因此,当进程退出的时候,它不能立即释放该进程对应的资源;它会保存一段时间,让父进程或者OS来进行读取!

那么,当进程退出了,而该进程的父进程或操作系统并没有对该进程进行回收,此状态叫做僵尸状态

例如我们模拟实现僵尸进程:创建子进程,父进程不退出,子进程正常退出,让父进程什么都不做

//zombie_Test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int count = 3;
        while(count--)
        {
            printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
        printf("子进程结束,进入僵尸状态\n");
    }
    else if(id > 0)
    {
        //父进程
        while(1)
        {
            printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(3);                                                                          
        }
    }
    return 0;
}
gcc zombie_Test.c -o zom.exe
./zom.exe

我们创建一个循环打印进程状态的指令:

while :; do ps ajx | head -1 && ps ajx | grep zom.exe | grep -v grep | grep -v sys; sleep 1; echo "-----------------------------"; done

此时我们左边窗口执行该程序,而右边窗口监视进程状态:

image-20230414152408591

通过观察进程状态的变化,我们知道当我们该程序子进程结束后,由于父进程为为它进行回收操作,这时候该子进程即成为了僵尸进程


僵尸进程危害:

僵尸进程不占用 CPU 或内存资源,但它们占用了系统中有限的进程号资源,而且如果大量的僵尸进程积累,会导致系统进程表满,无法再创建新的进程,从而影响系统的稳定性和可用性。

另外,僵尸进程还会占用一定的系统内存,因为它们的 PCB(进程控制块)并没有被完全清除,只是在系统中被保留,等待其父进程来回收它们。如果父进程不及时回收僵尸进程,这些 PCB 就会一直存在于系统中,占用内存资源。(即:僵尸进程一直存在就会造成内存泄漏

此外,僵尸进程还会对系统的安全性产生潜在的威胁,因为它们的存在可能会被恶意程序利用,从而导致系统遭受攻击。

因此,及时清除僵尸进程对于系统的稳定性、可用性和安全性都非常重要。一般情况下,父进程应该在子进程结束后调用 wait() waitpid() 系统调用来回收僵尸进程,以保持系统的稳定性和可用性。

6.4 孤儿进程

在 Linux 系统中,当一个进程的父进程在该进程结束之前先结束了,那么这个进程就成为孤儿进程(Orphan Process)

1、这种现象是一定存在的

2、子进程此时会被操作系统接管——进程号为1

3、为什么要接管该进程:如果该子进程退出了,成为僵尸进程,就没有对应父进程回收它了!

4、因此,这个被接管的进程,叫做孤儿进程

5、如果前台进程创建的子进程,成为了孤儿进程,那么它会自动变成后台进程

例如我们模拟实现孤儿进程:让父进程结束,子进程继续运行

//nofather_Test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
    }    
    else if(id > 0)    
    {    
        //父进程    
        int cnt = 3;    
        while(cnt--)
        {                                                                     
            printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
        
        printf("父进程结束,子进程变成孤儿进程!\n");
    }
    return 0;
}
gcc zombie_Test.c -o nof.exe
./nof.exe

同样的,我们还是使用循环打印进程状态的指令:

while :; do ps ajx | head -1 && ps ajx | grep nof.exe | grep -v grep | grep -v sys; sleep 1; echo "-----------------------------"; done

此时我们左边窗口执行该程序,而右边窗口监视进程状态:

image-20230414154522970


当我们想通过Ctrl C的方式终止子进程时,发现没有用:

Ctrl C只能终止前台进程,由于该前台进程创建的子进程成为了孤儿进程,此时子进程变成后台进程,此时无法通过Ctrl C的方式终止该进程

+代表该进程在前台,而父进程执行后没有+,说明该进程为后台进程,无法被Ctrl C终止

image-20230414155138397

我们只能使用kill指令终止该进程:

kill -9 [PID]     #输入该子进程pid结束该进程

image-20230414155000681

关于kill指令

kill指令是给目标进程发送信号

格式:kill -[选项/信号编号] 进程PID

可以通过kill -l来查看kill指令的信号编号(选项)

image-20230420144132277

而我们最常使用的,是9号选项,即终止进程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/444491.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nssctf web 入门(9)

[SWPUCTF 2021 新生赛]sql [SWPUCTF 2021 新生赛]sql 可以猜测做了过滤 这里可以bp跑一下看过滤了哪些 尝试发现过滤了空格/**/在sql中代表注释符在mysql中这个可以用来代表空格 发现--被过滤 #也不行我们试下url编码的#也就是%23 成功了 3个字段 得到库名test_db 这里提示非…

一篇文章让你彻底学会--防抖(并且自己可以手写)

Hi,有的小伙伴们在面试的时候会被要求手写防抖函数&#xff0c;很多都被难着了吧&#xff0c;宝贝&#xff0c;那你你没有理解防抖函数。 今天&#xff0c;就让我带你攻克它&#xff01; 1.防抖 防抖:单位时间内&#xff0c;频繁触发事件&#xff0c;只执行最后一次。 人话:说…

嵌入式Linux(1):make menuconfig图形化界面

文章目录 搜索功能配置驱动状态退出和make menuconfig有关的三个文件make menuconfig会读取哪个目录下的Kconfig文件&#xff1f;为什么要复制成.config而不复制成其他的文件呢&#xff1f;在默认的deconfig上面通过make menuconfig来改配置&#xff0c;然后保存怎么和Makefile…

门店智能经营平台能解决哪些问题?应该如何选购?

现在很多实体店都开始借助第三方软件或系统&#xff0c;来为自家门店搭建门店智能经营平台&#xff0c;来智能管理自家门店的商品和库存&#xff0c;提高门店运营效率。 一、使用门店智能经营平台能解决哪些难题&#xff1f; 1、实现数据化决策 通过门店智能经营平台&#xff…

VUE3的使用

文章目录 一、Vue3基础语法1、Vue开发准备2、Vue的模板语法3、条件渲染4、列表渲染5、事件处理6、表单输入绑定 二、Components组件1、组件基础2、组件交互3、自定义事件的组件交互4、组件生命周期5、引入第三方组件 三、网络请求及路由配置1、Axios网络请求2、Axios网络请求封…

【Python_Matplotlib学习笔记(一)】pyplot模块的基本用法

pyplot模块的基本用法 前言 Matplotlib 是一个 Python 的 2D绘图库&#xff0c;它以各种硬拷贝格式和跨平台的交互式环境生成出版质量级别的图形&#xff08;来自&#xff1a;百度百科&#xff09;。pyplot 模块是 Matplotlib 模块提供的快速绘图模块&#xff0c;它模仿了 MA…

H12-831最新解析25题

401、(判断)isis silent命令用来配置IS-IS接口为抑制状态&#xff0c;即抑制该接口接收和发送IS-IS报文&#xff0c;但此接口所在网段的路由可以被发布出去。 A.TRUE B.FALSE 答案&#xff1a;A 解析&#xff1a;isis silent命令为设置静默接口&#xff0c;配置了之后该接口…

5.1、阻塞/非阻塞、同步/异步(网络IO)

5.1、阻塞/非阻塞、同步/异步&#xff08;网络IO&#xff09; 1.阻塞/非阻塞、同步/异步(网络IO)①典型的一次IO的两个阶段是什么&#xff1f; 2.日志系统①基础知识②整体概述③本文内容④单例模式1.经典的线程安全懒汉模式2.局部静态变量之线程安全懒汉模式 ⑤饿汉模式⑥条件…

Tomcat 部署与优化

1. Tomcat概述 Tomcat是Java语言开发的&#xff0c;Tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;是Apache软件基金会的Jakarta项目中的一个核心项目&#xff0c;由Apache、Sun和其他一些公司及个人 共同开发而成。Tomcat属于轻量级应用服务器&#xff0c;在…

电商系统架构设计系列(一):电商系统到底是如何设计出来的?

引言 电商这个业务&#xff0c;和我们的生活息息相关。你可能对电商多少有一些了解&#xff0c;但是&#xff0c;即使是一个最小化的电商系统&#xff0c;它仍然非常复杂。在这个系列的文章里&#xff0c;我们将一起以一个创业公司的 CTO 的视角&#xff0c;来设计一个最小化的…

深度学习实战26-(Pytorch)搭建TextCNN实现多标签文本分类的任务

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下深度学习实战26-(Pytorch)搭建TextCNN实现多标签文本分类的任务&#xff0c;TextCNN是一种用于文本分类的深度学习模型&#xff0c;它基于卷积神经网络(Convolutional Neural Networks, CNN)实现。TextCNN的主要思想…

C语言实现链表--数据结构

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1…

gateway整合knife4j(微服务在线文档)

文章目录 knife4j 微服务整合一、微服务与单体项目文档整合的区别二、开始整合1. 搭建一个父子maven模块的微服务,并引入gateway2.开始整合文档 总结 knife4j 微服务整合 由于单个服务的knife4j 整合之前已经写过了,那么由于效果比较好,然后微服务的项目中也想引入,所以开始微…

【Linux】多线程的互斥与同步

目录 一、线程冲突 二、重入与线程安全 1、线程不安全的情况 2、线程安全的情况 3、不可重入的情况 4、可重入的情况 5、可重入和线程安全的联系 6、STL是否线程安全 7、智能指针是否线程安全 三、互斥锁 1、互斥锁的使用 2、基于RAII风格的互斥锁的封装 2.1Mutex…

ChatGPT-4回答电子电路相关问题,感觉它有思想,有灵魂,一起看看聊天记录

前几天发了一篇文章&#xff0c;讲了我们平常摸电脑或者其它电器设备的时候&#xff0c;会有酥酥麻麻的感觉&#xff0c;这个并不是静电&#xff0c;而是Y电容通过金属壳泄放高频扰动&#xff0c;我们手摸金属壳的时候&#xff0c;就给Y电容提供了一个泄放回路&#xff0c;所以…

全网抓包天花板教程,CSDN讲的最详细的Fiddler抓包教程。2小时包你学会

目录 前言 一、安装 Fiddler 二、打开 Fiddler 并设置代理 三、抓取 HTTP/HTTPS 流量 四、流量分析和调试 五、应用场景 六、注意事项 七、实际案例 八、拓展阅读 九、结论 前言 Fiddler 是一款功能强大的网络调试工具&#xff0c;可以用于捕获和分析 HTTP 和 HTTPS …

生物信息学有哪些SCI期刊推荐? - 易智编译EaseEditing

以下是几个生物信息学领域的SCI期刊推荐&#xff1a; Bioinformatics&#xff1a; 该期刊是生物信息学领域最具影响力的SCI期刊之一&#xff0c;涵盖了生物信息学、计算生物学、系统生物学、生物医学工程等多个研究方向。 BMC Bioinformatics&#xff1a; 该期刊是生物信息学…

数据结构入门(C语言版)二叉树链式结构的实现

二叉树链式结构的实现 二叉树的概念及结构创建1、概念2、结构创建2、创建结点函数3、建树函数 二叉树的遍历1、前序遍历2、中序遍历3、后序遍历4、层序遍历 二叉树的销毁结语 二叉树的概念及结构创建 1、概念 简单回顾一下二叉树的概念&#xff1a; ★ 空树 ★非空&#xff1…

intellij 从2020升级到2023 踩坑实录

1.下载新版本intellij 工作机器上的intellij版本为2020社区版&#xff0c;版本比较老旧&#xff0c;需要进行升级。IDE这种提高生产力的工具&#xff0c;还是蛮重要的&#xff0c;也是值得稍微多花点时间研究一下的。升级之前就预计到了不会是那么简单&#xff0c;后面事实也证…

大型体检管理系统源码,Vs2012,C/S架构

体检管理系统源码&#xff0c;PEIS源码 一套专业的体检管理系统源码&#xff0c;核心功能有体检档案的录入、体检报告的输出、体检档案的统计查询和对比分析。该系统的使用&#xff0c;可以大大提高体检档案管理人员的工作效率&#xff0c;使体检档案的管理更加准确、全面、完…