Linux进程初识
本文已收录至《Linux知识与编程》专栏!
作者:ARMCSKGT
演示环境:CentOS 7
前言
进程是计算机中的重要概念,一个程序被操作系统加载进入内存那么这个程序就成为进程,一个程序可以启动多次产生多个进程,操作系统也要管理这些进程,本节将介绍关于进程的一些基本知识!
正文
冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系,冯诺依曼早在二十世纪四十年代就提出了这种结构。
冯诺依曼体系结构将计算机的组成分为五大部分:
- 输入设备:键盘,摄像头,麦克风,硬盘,网卡等。
- 存储器(内存):只读存储器(如BIOS系统),随机读写存储器(我们生活中使用的内存)。
- 运算器:负责数据的处理计算,是CPU的一部分。
- 控制器:协调整个电脑有序工作,是CPU的一部分。
- 输出设备:显示器,扬声器,硬盘,网卡等。
其中这五大部分可以总结为三个设备
- 外部设备:输入输出设备
- 存储器:内存
- 中央处理器(CPU):运算器+存储器
很早以前,计算机并没有存储器这个设备,CPU直接跟外设打交道,但由于外设比较慢,CPU经常都处于空闲状态,这样使得CPU资源被极大的浪费,为了提高CPU资源的使用效率,冯诺依曼提出在CPU和外设之间增加一个存储器!
存储器是断电易失设备,也就是说存储器上的数据断电后下次启动就没有了,这里要区别与硬盘。虽然存储器断电易失数据,但是其速度非常快(相对于外设来说);如每次我们启动程序时将程序先预加载到存储器上,CPU在需要某些程序资源时直接去存储器中取即可(避免频繁与外设I/O),这样就不需要与外设打交道,这样就极大的提高了CPU资源的使用效率!
冯诺依曼体系结构中CPU只需要与存储器打交道,需要从外设读取资源时通过存储器向对应设备发送指令然后将数据加载到内存处理即可,而外设只需要和存储器打交道就行,这样通过添加了一个硬件层提高了计算机整体的运行效率!
总的来说,冯诺依曼体系结构下,一般情况CPU只与存储器打交道,外设只与存储器打交道,所以程序必须被加载到内存上才能运行是由冯诺依曼体系结构决定的!
操作系统简介
概念
我们普通用户无法直接与计算机中的硬件打交道,也就是说在没有操作系统的情况下,我们几乎是无法使用计算机的,于是计算机大佬们创造了各种各样的操作系统!
目前常见的操作系统:
- Windows操作系统
- Linux操作系统
- Mac操作系统(基于Unix)
- Android操作系统(基于Linux)
- …等
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序,应用软件等等)
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
OS的作用:
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
操作系统的管理
对于一个公司来说,可以分为以下层级
公司中有很多部门,每个部门有很多员工,为了更好的管理所有的部门和员工,这些部门需要编号,每个部门的员工也需要编号,且部门与员工也需要有对应的独立信息,相当于我们的身份证一样!
这种记录信息的方式我们一般称为描述某信息的存储方式,在C/C++语言中一般用结构体去存储!
最后公司使用一个系统将信息存储起来,发生信息变动时对这个系统的信息进行增删查改即可!
这种存储所有信息的方式我们一般称为 组织 所有信息的存储,在所有语言中一般使用 各种高效的数据结构 进行存储!
通过上述的方式,我们对所有信息的管理就转变为了对数据结构的增删查改!
所有操作系统对信息的管理方式是:先描述,再组织!
因此,操作系统管理进程也是使用此手段进行管理!
在Linux系统中,一个进程有自己的ID(一般称为pid),内存空间地址等,这些信息使用结构体进行描述,然后使用链表进行组织!
系统调用
在Linux系统中,如果我们要访问硬件,例如磁盘,是不可能通过软件跨过操作系统直接访问,而是借助操作系统的帮助去访问!
这种借助操作系统接口(函数)去实现某个功能的方法,称为系统调用!
而软件调用系统调用的大致逻辑是:软件 → 操作系统 → 硬件驱动 → 硬件
系统层次结构:
通过上图,我们位于开发操作层级,也是用户层级,来开发各种方便的软件提供给上一层的用户群体!
所以操作系统对下通过管理好软硬件资源的手段,来达到对上给用户提供良好(安全,稳定,高效,功能丰富等)的执行环境的目的。
注意:操作系统给我们提供非常良好的服务,并不代表操作系统相信我们,反而操作系统不相信任何人,就像我们去银行办业务一样,银行提供各种非常良好的服务,但是不会让你接触到例如金库和用户账户信息这样的核心资源!
系统调用和库函数的概念
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程初识
通过上面操作系统的简介,我们可以发现操作系统内核有四个模块:
- 内存管理
- 进程管理
- 文件管理
- 驱动管理(设备管理)
这里我们对操作系统进程管理进行初步简介!
进程理解
进程 = 内核(关于进程相关的)数据结构 + 当前程序的代码和数据
任何启动并运行程序的行为都是由操作系统帮助我们将程序转化为进程完成特定的任务并将程序加载到内存上通过CPU计算运行得出结果的过程!
可执行程序本质是一个普通的二进制文件,也就是说本质程序也是文件那么:文件 = 内容 + 属性。这里属性就是进程在操作系统中描述进程的信息(进程控制块PCB),内容则是程序所需要的代码和资源文件等!
通过上面操作系统的介绍,操作系统管理进程的方式是先描述再组织进行管理。每创建一个进程就会将程序的信息和属性读取到这个结构体,根据进程的属性设置结构体。而Linux操作系统中使用双向循环链表对所有的进程进行管理!
进程的属性和数据
进程的数据就是程序的代码以及需要使用的各种资源文件!对于我们来说,进程的数据大部分都是代码。
我们编写程序例如使用C/C++语言形成 .c/.cpp 文件,经过编译得到一个可执行,通过操作系统运行生成相应的进程属性(进程控制块PCB),并执行二进制指令得到程序的运行结果!
进程控制块
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。一般我们将进程控制块称之为PCB(Process Control Block),Linux操作系统下的PCB是:task_struct。
PCB中的属性与程序文件属性几乎无关,PCB中存储的文件属性并不是磁盘存储时文件的属性,而是根据其内容动态创建的,所以文件属性与文件本身属性几乎无关!
task_struct是PCB的一种,其中:
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct包含的内容:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
我们启动一个程序是通过 路径+可执行程序名 启动的,这也是为什么我们在Linux下运行一个程序需要加 ./,因为Linux系统并不知道我们在当前哪一个文件夹,所以我们需要想系统交代清楚!
当我们启动一个程序时,操作系统将可执行程序加载到内存中,创建对应的PCB,然后将其组织起来!
查询进程信息的相关指令
ps指令
ps指令会简略的显示当前系统运行的部分进程信息
但这并不是ps的常用方法;ps常用方法是搭配选项axj(或ajx)显示全部进程的详细信息然后通过其他指令筛选!
指令:ps ajx | head -1 && ps ajx | grep 进程名 | grep -v grep
其中:
- ps ajx | head -1 是取头标题
- ps ajx | grep 进程名 | grep -v grep 是从所有进程中筛选出指定进程名的进程信息并去掉grep进程信息
top指令
top指令相当于Linux下的任务管理器,显示进程的详细信息,而且是动态更新的,输入 q 退出,在任务管理器下可以进行各种操作!
ls /proc 查看进程控制块目录
/proc目录是一个内存级目录,不会存储在磁盘上,在系统启动时生成,Linux中所有的task_struct都会存放在这个目录中(但不只存储task_struct),每一个task_struct以进程PID进行命名!
进程PID
进程的PID相当于每个进程的身份证号,是独一无二的!对于后面的一些进程控制需要使用PID控制子进程!
#include <sys/types.h> //系统调用所需头文件 #include <unistd.h> pid_t getpid (void) //返回当前进程的PID,不需要任何参数
#include <iostream> using namespace std; #include <sys/types.h> #include <unistd.h> int main() { while(true) { cout << "这是一个PID为" << getpid() <<"进程..." << endl; sleep(1); //睡眠一秒再执行 - 每秒打印一次 } return 0; }
注意:程序是唯一的,而进程可以有多个,一个程序可以启动多次生成多个进程,每个进程都是独立的,所以PID也不同,即我们每次启动同一个程序时进程的PID是不同的!
父子进程
父子进程及PPID
进程间存在父子关系,一个进程可以创建子进程执行其他任务!
在Linux中我们启动的大部分程序都是通过Bash帮我们去执行的,也就是说我们启动的几乎所有程序都是Bash创建的子进程去运行的,这样的设计也是为了防止恶意程序破坏内核的运行!
这里需要介绍一个新系统调用,getppid用于获取当前进程的父进程PID,在查询进程信息中PPID就是父进程的PID。#include<unistd.h> //系统调用所需头文件 #include<sys/types.h> pid_t getppid(void); //用法与getpid保持一致
我们修改上面的代码进行运行
int main() { while(true) { cout << "这是一个 PID为" << getpid() << " PPID为" << getppid() <<" 的进程..." << endl; sleep(1); //睡眠一秒再执行 - 每秒打印一次 } return 0; }
fork创建子进程
fork函数是一个系统调用,用于创建子进程
#include <unistd.h> //系统调用头文件 pid_t fork(void);
对于fork函数有以下特性:
- fork函数形象上有两个返回值,对于父进程fork会返回其子进程的PID,对于子进程会返回0;**如果子进程创建失败会向父进程返回 -1 **。
- fork函数创建子进程可以嵌套,也就是说可以在子进程中继续创建子进程。
- 父进程可以一次性使用fork创建多个子进程。
int main() { pid_t id = fork(); if(id == -1) //创建失败 直接退出 { cout << "进程创建失败!" <<endl; exit(1); } if(id == 0) //如果id为0则是子进程 开始执行子进程代码块 { cout<<"子进程所接收的id值为:"<<id<<endl; while(true) { cout<<"我是子进程,我的PID:"<<getpid()<<"我的PPID:"<<getppid()<<endl; sleep(1); } } //运行到这个地方的必定是父进程 cout<<"父进程所接收的id值为:"<<id<<endl; while(true) { cout<<"我是父进程,我的PID:"<<getpid()<<"我的PPID:"<<getppid()<<endl; sleep(1); } return 0; }
fork系统调用原理
父进程在创建子进程时会先创建一个pcb以父进程为模板拷贝大部分属性但不是全部,这样就在内核中创建另一份pcb并共享父进程的代码和数据。
进程具有独立性,相互之间互不干扰,包括父子进程,所以如果父进程创建子进程后先被关闭,则子进程也不会受到很大影响,只不过子进程由于失去了父进程会由Bash直接接管成为孤儿进程。
由于进程具有独立性,当两个进程在函数域中共用一个变量时,一开始两个进程同时使用这个变量,如果只是读取则不会发生什么变化,如果一方修改,则修改的一方操作系统会为其新开辟一块该变量的空间并将变量内容复制过去,然后修改方进程自由进行修改,这样两个进程的数据不会受影响,这个过程称为写时拷贝。
所以父子进程代码共享(因为代码数据是只读),但数据是写时拷贝各自一份。
孤儿进程验证
向进程发送9号信号关闭该进程,指令:kill -9 进程PID
后台进程无法被使用CTRL+C关闭,所以需要使用kill -9强制关闭后台进程。
写时拷贝验证
int main() { pid_t id = fork(); int num = 0; if(id == -1) //创建失败 直接退出 { cout << "进程创建失败!" <<endl; exit(1); } if(id == 0) //如果id为0则是子进程 开始执行子进程代码块 { for(num = 1;num<=5;++num) //子进程运行10次结束 { cout<<"子进程num:"<<num<<endl; sleep(1); } cout<<"子进程退出!"<<endl; exit(1); //子进程运行结束直接退出不需要执行父进程的代码 } //运行到这个地方的必定是父进程 while(true) { cout<<"父进程num:"<<num<<endl; sleep(1); } return 0; }
这里可以发现子进程修改了num但并未影响父进程!
关于写时拷贝的细节涉及虚拟地址空间,我们后期会详细介绍!
关于父子进程小结
- bash 命令行解释器本质上也是一个进程,可以被销毁。
- 命令行启动的所有程序都是bash的子进程,其父进程都是 bash,因为Linux系统通过这种方式避免内核被破坏。
- 父进程被销毁后,子进程会变成孤儿进程。
- 进程间具有独立性,包括父子进程,当双方共用数据时一旦有一方修改就会触发写时拷贝。
最后
进程初识的介绍到这里就差不多结束了,本节我们简单的介绍了进程的相关知识,介绍了冯诺依曼体系结构,操作系统对于数据的管理方式,进程的理解以及信息的查看,最后说明了关于父子进程的相关知识,说明了进程间具有独立性的问题,关于进程的知识这只是冰山一角,后面我们会展开进行详细介绍!
本次 <Linux进程初识> 就先介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
🌟其他文章阅读推荐🌟
C++ <STL之string的使用>
C++ <模板>
C++ <内存管理>
C++ <类和对象 - 下>
C++ <类和对象 - 中>
C++ <类和对象 - 上>
🌹欢迎读者多多浏览多多支持!🌹
- 点赞
- 收藏
- 关注作者
评论(0)