Linux进程概念
前言
博主学完linux之后,站在完整的体系上,重新回顾进程的概念,有了不少新的理解与体会。
本文将介绍简单的硬件设施,冯诺依曼体系结构引入进程概念。同时将介绍进程的底层概念PCB字段、常见查看进程状态的方式,OS指导下的进程状态,以及linux下进程的状态。
冯诺依曼体系结构
冯诺依曼体系结构是当今最普遍的硬件结构。
各部分的描述:
输入设备:显卡、键盘、鼠标、摄像头、ssd等等。
输出设备:显示屏、扬声器、网卡等等。
存储器:就是常见的内存,在硬件上常常又叫做内存条。
中央处理器(CPU):由运算器和控制器构成。
关于这些硬件,建立一个认知
冯诺依曼体系结构种的所有硬件设置,都有存储数据的能力。只不过是大小的区别。
离CPU越近的硬件,拷贝速度越快。
实际上,所有的IO数据都必须先被加载到内存中,再由CPU运算。
那么如果CPU直接去调用IO,必然会受到IO效率的影响。将CPU的速度下降几千倍。所以CPU之和内存打交道,所有数据必须先被加载到内存中,才能被CPU处理。
这就好比木桶原理:IO设备是最短的木板,CPU是最高的木板,所以装水量取(计算速度)决于CPU。
硬件存储数据的效率:CPU>内存>输入和输出设备
所以再回过头来,什么是冯诺依曼体系结构?
冯诺依曼体系结构规定硬件的组成是:输入设备,输出设备,内存,CPU(中央处理器)。
CPU只和内存打交道,数据必须先被加载到内存才能被处理。这样的好处:可以利用较低的成本,组装出一台效率还不错的计算机。
举例:描述在双方主机利用软件聊天时的数据流动
管理的概念
linux内核下必然存在大量的硬件,比如显卡、网卡、显示器等等。一套尽然有序的计算器,必然是需要一个管理者将这些零碎的部件组织起来的。就像一个学校,被分为各种小班级,对每个班级管理好了,也就能对所有的学生管理好。
先描述:
硬件属于最底层的,我们要想知道这个硬件是什么?靠的是外界对硬件属性的描述。
比如一个人,你对他的描述(男,18岁,学生)。
所以要对一个事物的管理就需要先进行描述对象。
在语言层面对事物的描述,就是一个struct结构体,结构体包含事物属性的字段。
//简单的描述一个人
struct Person(){
string name ,
int age,
int birthday
};
通过结构体的形式描述对象的属性就能清楚的认识一个事物
因此,就能将各个硬件设备描述起来。
再组织
然而只有描述某一个同学是不够的 ,为了管理的井然有序,这位同学必然属于某个班级,同时这位也能属于某个社团。
这一个将同学划分区域的方法,就叫做组织。
对应语言层次:无非就是用特定的数据结构,将一个数据放入。
然后就能对数据进行增删查改!
常见的组织方式:链表、哈希表、vector
管理的核心!
先描述,再组织。是一种哲学的指导思想
操作系统管理的核心模块
- 进程管理
- 内存管理
- 文件管理
- 驱动管理
操作系统
之前谈到的管理者就是操作系统。
操作系统就是硬件资源的管理者。并且为用户提高一个稳定的运行环境。
仔细看这张图
最底层是由硬件构成,而在硬件之上是由各种驱动对硬件辨识。对硬件进行控制。驱动的好处就是能够将不同型号的硬件适配。
而在驱动层之上就是四个管理层,也就是操作系统 。所以说操作系统就是对硬件资源的统一管理。
理解为用户提供稳定的运行环境
操作系统是对底层硬件的管理。实际上,操作系统是不信任用户的,就是决定,操作系统不会暴露、公开直接管理的数据。但是如果用户依旧想访问外设呢?比如往显示屏上写数据,但是又不能直接操作外设,只能通过操作系统去替我们间接访问外设。
这一个过程就是系统调用,通过系统调用去访问硬件设备,才能保证OS内的稳定安全!
什么是操作系统?
管理硬件资源,并且为用户提供一个安全稳定的运行环境。
而在系统调用之上,还存在一层用户操作。
这一层存在的目的是:有时候系统调用并不方便,因此就会封装出第三方库,比如C语言的fopen就必然封装了 系统调用open。
因此更换这一层就会产出不同的shell外壳:ubantu、CentOS等
进程概念
什么是进程
比如我们在磁盘上创建一个test.cc文件,通过编译链接之后生成可执行文件,这就是程序。
当我们要运行的时候,由于冯诺依曼体系结构规定,必须要先加载到内存中。而在内存中必然会存在大量的从磁盘拷贝而来的可执行程序,对这些程序的管理。就是先描述,再组织!
描述进程属性的结构就是PCB(进程控制块)。然后利用特定的双向链表将PCB组织起来,这就是进程。
进程=代码数据(可执行程序)+PCB
进程控制块
OS对内存中大量可执行程序的管理是通过先描述,再组织的方式。进程控制块PCB就是描述可执行程序的结构体。在Liunux系统下的进程控制块叫做task_struct。
task_struct包含进程的描述字段,比如:id,代码地址,数据地址,进程状态,优先级,next指针等等。所以通过task_struct就能找到对应的代码+数据。
对进程的管理就转化为对task_struct的管理。
见一见linux内核0.11下的task_struct
struct task_struct {
/* these are hardcoded - don't touch */
// 进程状态
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
// 计数器
long counter;
// 进程优先级
long priority;
// 进程获取的信号
long signal;
struct sigaction sigaction[32];
// 进程阻塞的信号 bit map
long blocked; /* bitmap of masked signals */
/* various fields */
// 退出状态码
int exit_code;
unsigned long start_code,end_code,end_data,brk,start_stack;
long pid,father,pgrp,session,leader;
unsigned short uid,euid,suid;
unsigned short gid,egid,sgid;
long alarm;
long utime,stime,cutime,cstime,start_time;
unsigned short used_math;
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;
struct m_inode * pwd;
struct m_inode * root;
struct m_inode * executable;
unsigned long close_on_exec;
struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];
/* tss for this task */
struct tss_struct tss;
};
获取进程标识符
- getpid()获取进程的pid
- getppid()获取父进程的pid
创建一个进程方式有俩种:
- 通过执行exe文件
- fork创建子进程
而这些进程都是由父进程创建的,比如执行exe就是bash进程(命令行解释器)创建的子进程,
fork不用说,就是由当前进程创建的。
演示:
#include<iostream>
#include<unistd.h>
using namespace std;
int main(){
while(1){
std::cout<<"我是一个进程!我的pid是:"<<getpid()<<" ppid:"<<getppid()<<std::endl;
sleep(1);
}
return 0;
}
这个3395584进程就是bash
查看进程
查看进程的方式有俩种
- ps
- ls /proc
创建一个简单的进程,并且运行起来
演示:
ps -ajx 查看进程的状态
通过管道 抓取指定 test进程的状态
ps -ajx | grep test
将头部信息也显示
ps -ajx | head -1 && ps -ajx | grep test
这时候调用grep的时候,会生成一个greptest进程,通过-v 选项可以将grep过滤
通过proc查看
实际上,进程的运行的时候,会在 /proc文件中以文件的唯一标识:也就是pid 创建一个文件,文件的内容就是pcb结构体。看一看这个文件。
ls /proc/
果然,在/porc/目录下存在一个以pid为标识的文件。看一下文件的内容:
exe就是可执行文件在磁盘中的位置
- cwd就是当前进程的位置
这就是进程在运行时候,就能确定的字段,都保存在proc的文件中。
比如调用fopen,就能在当前目录下创建新文件,就是和这个字段有关系。
如果希望修改文件的路径,可以通过chdir系统调用,修改的就是动态文件下的cwd。
这个以pid命名的文件是动态的,一旦关闭进程,这个动态文件就会自动删除。
fork创建子进程
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
用于创建子进程
返回值
- 失败返回-1
- 对父进程返回子进程的pid
- 子进程返回0
一个函数有俩个返回值是比较特殊的,后续将进行解释。
演示:
#include<iostream>
#include <unistd.h>
using namespace std;
int main(){
std::cout<<"fork之前"<<std::endl;
pid_t id=fork();
if(id<0){
std::cout<<"fork失败!"<<std::endl;
}
else if(id==0){
while(1){
std::cout<<"我是子进程,pid:"<<getpid()<<",ppid:"<<getppid()<<std::endl;
sleep(1);
}
}
else{
while(1){
std::cout<<"我是父进程,pid:"<<getpid()<<",ppid:"<<getppid()<<std::endl;
sleep(1);
}
}
return 0;
}
fork之前的语句只会被执行一次,fork之后就会分流,子进程执行id==0的语句,父进程执行id>0的语言
创建子进程的目的:协助父进程完成任务。
关于fork的几个问题
fork的原理
- 以父进程为模板,创建子进程的PCB,子进程的PCB也会指向代码+数据
- 共享代码
为什么父进程返回子进程的pid,子进程返回0?
- 一个父进程可能会有很多的子进程,父进程需要给特定的子进程分派任务就是依靠id来区分。
- 而子进程只需要知道是否创建成功即可,它只有一个父进程。
为什么会有俩个返回值?
- 因为执行完fork的核心代码后,就有俩个执行流。俩个执行流共享代码,都会return。但是数据是不共享的,通过写时拷贝就会返回俩个值。
谁先运行?
不确定,由时间片,优先级和调度算法决定。
进程的状态
见一见操作系统的指导下的进程状态
重点讨论一下运行状态、阻塞状态和挂起状态
运行状态
所谓的状态无非就是PCB中的一个字段status,#defind RUN=1 #define BLOCK=2 ....这样的形式,而更改进程的状态就是把status的值修改
每一个CPU都会运维一个运行队列,而运行队列就是包含PCB的指针,如果要调度某一个进程,就从运行队列中取数据。
一个共识:
只要在运行队列中的PCB都是准备好的,随时都是可以被调度的,状态都是运行状态
只不过可能是时间片结束,暂时没有被调度。
验证运行状态
阻塞状态
阻塞状态也是OS中比较常见的状态,比如我们在访问硬件资源的时候,如果数据迟迟没有就绪,那么数据就获取不到,就必须等待。给用户的体验就像是卡住了。
产生阻塞的原因:
资源未就绪,不具备访问的条件。
详解阻塞状态
建立一个认知:
每一个设备都会存在一个等待队列。
挂起状态
如果进程访问的资源没有就绪,进程就会被阻塞等待资源的到来。而如果恰巧OS内的资源已经很少了,OS也快要挂掉了。这时候,被阻塞的资源不仅没有执行任务,反而占用内存资源。就有理由将该进程的代码+数据先移动到磁盘中。
产生的原因:
进程被阻塞了,并且OS内的资源严重不足,就会产生挂起!所以就称为阻塞挂起。
什么时候加载回来?
等到进程被OS调度的时候了,就会加载回来。
Linux内核下进程的状态
来见一见linux内核0.11下的进程状态
/*
* 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 */
};
- R状态:对应的就是OS中的运行状态,在Linux下存在R和R+状态,都代表运行状态,不同的是R+状态表示的是以前台进程运行,在运行的时候添加 &选项表示后台进程运行。
- S状态:休眠状态,浅度休眠,可以被信号终止。
- D状态:深度休眠,不能被信号终止。
S和D有什么区别?
本质都是阻塞状态,D状态又叫做磁盘阻塞。当用户往程序写入重要的数据的时候,如果当OS内存不足时候,OS会杀掉进程,但是这个写入是重要内容,不能被杀掉,所以就将进程设置为D深度休眠状态。
- T:暂停状态,通过向信号发送信号SIGSTOP将进程暂停,发送SIGCONT将进程继续运行。
- t:追踪状态,进程被gdb调式,就会阻塞住等待gdb的指令,这个等待期间,状态就是t。
- X:死亡状态,进程终止之后,父进程释放pcb资源,进程正真意义上的终止,就是一瞬间的状态。
- Z状态:进程终止后,会释放代码和数据,但是PCB内的资源暂时不会被释放。是为了告知父进程读取退出的原因,而父进程迟迟没有读取PCB资源,这一过程进程处于僵尸状态。
如果进程进入僵尸状态,而父进程一直不读取pcb资源,就会造成OS内存在大量的PCB资源,就会造成内存泄漏。
孤儿进程:
子进程都是由父进程创建的,子进程退出时,PCB资源由父进程读取。如果父进程提前退出,而子进程结束后,资源由谁读取?
OS内规定,如果父进程提前退出,子进程就会成为孤儿进程,由1号进程(bash)领养。
下文将介绍进程概念的重点:进程优先级,O(1)调度算法,环境变量,以及进程地址空间等等。