0
点赞
收藏
分享

微信扫一扫

【linux操作系统】进程的概念

德州spark 2022-04-05 阅读 97
linux

目录

冯诺依曼体系结构

操作系统

概念

设计OS的目的

如何理解“管理”

系统调用和库函数的概念

进程

进程的概念

描述进程的控制块-PCB

 PCB的内部构成

查看进程

通过系统调用创建进程-fork

 进程状态

 孤儿进程


冯诺依曼体系结构

计算机都是由硬件组成的

 


当两个不同地方的人是如何发消息的呢?

结论:任何外设,在数据层面,基本优先和内存打交道,CPU在数据层面上也直接和内存打交道

操作系统

概念

        任何计算机都包括一个基本的程序集合,称为操作系统(OS),而操作系统在启动下才有意义,因为要将数据与代码加载到内存中,那么OS是什么?OS就是一款专门针对软硬件资源进行管理工作的软件。再整个计算机硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。

概括的理解,操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(如库函数,shell程序等)

设计OS的目的

  • 对下,与硬件交互,管理所有软硬件资源,
  • 对上,为用户提供一个稳定的,高效的,安全的运行环境

如何理解“管理”

管理分为管理者和被管理者,而管理包括作决策的和执行者

举例子校长就是典型的管理者,辅导员是执行者,学生就是被管理者

  1.  管理者和被管理者并不直接打交道,而是通过执行者来获取被管理者的数据。
  2. 如何管理被管理者呢?对被管理者做出各种决策,决策是要有依据的

        那么操作系统就是管理者,它并不直接管理硬件或软件,而是通过执行者来对用户和硬件进行管理,用户操作接口和驱动程序就是执行者。

        那么如何管理进程呢?先描述,再组织。描述进程是通过描述进程的结构体,这个进程控制结构体叫做进程控制块(PCB)。

        操作系统:对下管理好软硬件资源,对上为用户提供良好的运行环境-普通用户-程序员-为程序员提供各种基本功能。

        然而操作系统并不相信任何用户,但是它还要给用户提供服务,这就相当于银行,银行本身是不信任你的,但是他依旧会给你提供服务,但是不会直接提供,而是通过银行柜台来间接提供服务。所以操作系统并不会直接向用户提供服务,而是通过系统调用接口来给用户提供服务,叫做OS提供的接口。

系统调用和库函数的概念

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

进程

进程的概念

  • 书本概念:加载到内存的程序,叫做进程。程序的一个执行实例,正在执行的程序等
  • 内核概念:担当分配系统资源(CPU时间,内存)的实体

【问题】:系统中中可不可能存在大量的进程?可能

                是操作系统在管理进程吗?  是的

                如何管理呢?先描述,再组织

                用什么描述呢?此时描述作为一个结构体,我们称之为PCB,进程控制块。

                为什么要有PCB?任何进程再形成之时,操作系统要为该进程创建PCB

描述进程的控制块-PCB

        进程信息被放在一个叫做进程控制块的数据结构当中,可以理解为进程属性的集合。操作系统层面上,PCB就是一个结构体类型。在Linux系统中,PCB的形式为struct task_struct{}在这个结构体中,包含了进程所有的属性。PCB是属性的统称,那么struct task_struct就是具体表述这种属性的结构体。

        当运行这个程序,用命令行查看发现运行程序的时候,myproc程序存在pid,而停止运行程序,myporc.c程序的pid也不存在了。

[wjy@VM-24-9-centos test_3_21] cat myproc.c
#include <stdio.h>
#include<unistd.h>

int main()
{
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}

对于加载到内存的可执行文件和描述进程的结构体task_struct我们统称为进程,而tast_struct由操作系统自动创建,总的来讲进程=程序+操作系统维护进程的相关数据结构

 代码和数据加载到内存后会形成一个一个的task_struct,操作系统并不会对代码和数据进行管理,而是对代码和数据所对应的控制块进行管理。

 PCB的内部构成

1.标识符:描述本进程的唯一标识符,用来区别其他进程。pid

那我们来验证一下pid,获取pid要包含头文件,这个是系统头文件

[wjy@VM-24-9-centos test_3_21]$ cat myproc.c
#include <stdio.h>
#include<unistd.h>
#include <sys/types.h>

int main()
{
while(1)
{
printf("hello world!: pid: %d\n",getpid());
sleep(1);
}
return 0;
}

当一个进程执行,发现他的标识符pid是14793,用ps axj命令也验证了这一点。

除了ctrl+c可以结束进程,还有一个命令 kill -9 "pid"

 除了子进程pid还有父进程ppid,用getppid获取

printf("hello world!: pid: %d, ppid: %d\n",getpid(),getppid());

 这个ppid就是bash

在程序运行退出的时候总有一个return 0,那么这个就是退出码,通过echo $?查看退出码为100(因为代码程序已经将退出码改为100),但是第二次使用echo $?命令发现退出码变成0,这是因为echo $?命令会显示最近一条命令程序的退出码,第二次echo $?显示的是上一次用echo $?的退出码,为0.

[wjy@VM-24-9-centos test_3_21]$ cat myproc.c
#include <stdio.h>
#include<unistd.h>
#include <sys/types.h>

int main()
{
while(1)
{
printf("hello world!: pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
return 100;
}

 2.状态:任务状态,退出码,推出信号等

上面提到了退出码,运行程序时,会将程序和数据记录到pcb当中,同样退出码也会记录到pcb,然后让其他进程来获取这个状态。

所以task_struct中已经包括了

struct tack_struct
{
int pid;//进程标识符
int code,exit_code;//退出码
int status;//状态
}

 3.优先级:优先级和权限的区别

4.程序计数器:程序中即将被执行的下一条指令的地址,当操作系统执行完指一条指令,指针会自动跳转到下一个要执行的语句。

5.内存指针:包括程序代码和进程相关的数据的指针,还有和其他进程共享的内存块的指针。系统通过内存指针指向的这个信息来找到对应的数据信息,也就是说,系统通过内存指针指向的pcb找到对应的代码和数据。

6.上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]

当进程1走完这个时间片的进程,需要在队列后面排队,但是走的时候需要将已经运行的进程信息保存下来,以便下次继续运行时候衔接上次运行的结果继续运行,这就叫保护上下文。虽然寄存器只有一个,但是寄存器里的数据是你这个进程的。 当这个队列又轮到进程1来运行,进程将task_stuct中保存的上下文继续交给寄存器来运行,直到5ms结束后,再次将上下文保护起来。将上下文交给寄存器运行的这个过程,叫做恢复上下文

所以保护和恢复上下文是为了让你去做其他的事情,但不耽误当前的事情,并且当你想回来继续学习的时候,可以接着之前你的学习内容继续学习

所以上下文就是:进程执行时所形成的处理器寄存器当中与进程强相关的临时数据

通过上下文,我们能感受到进程是被切换的。

7.I/O状态信息:包括显示显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表,将进程写入写出的操作

8.记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

查看进程

进程的信息可以通过/proc系统文件夹查看

蓝色部分字体是进程id,也就是说进程启动之后,会在proc目录下形成一个以自身pid编号为目录文件名的文件夹

 当执行一个文件时,查看他的进程,就是查看在proc目录下的pid目录文件内容

 通过ls /proc/pid -al命令可以查看到,exe对应的就是执行该文件的路径,cwd是当前工作目录

 当在myproc.c中写了这样一个程序,发现在该程序的当前路径下出现了要写入的log.txt文件,最初是没有log.txt文件的。但是在写入这个程序后,该路径又出新了log.txt,我们并没有指明该文件的路径,该文件是如何生成在此路径下呢?

进程通过cwd找到当前路径,然后在这个路径下创建文件。

[wjy@VM-24-9-centos test_3_21]$ cat myproc.c
#include <stdio.h>
#include<unistd.h>
#include <sys/types.h>

int main()
{
FILE* fp = fopen("log.txt","w");
fclose(fp);
while(1)
{
printf("hello world!: pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
}
return 0;;
}

[wjy@VM-24-9-centos test_3_21]$ ./myproc
hello world!: pid: 9042, ppid: 6957
hello world!: pid: 9042, ppid: 6957
hello world!: pid: 9042, ppid: 6957
^Z
[1]+ Stopped ./myproc
[wjy@VM-24-9-centos test_3_21]$ ll
total 20
-rw-rw-r-- 1 wjy wjy 0 Mar 29 22:05 log.txt
-rw-rw-r-- 1 wjy wjy 64 Mar 21 11:20 Makefile
-rwxrwxr-x 1 wjy wjy 8616 Mar 29 22:05 myproc
-rw-rw-r-- 1 wjy wjy 240 Mar 29 22:04 myproc.c

通过系统调用创建进程-fork

fork有一下特点:

  • fork有两个返回值
  • 父子进程代码共享,数据各自开辟空间(采用写实拷贝)

通过下面三个代码发现,第一个hello world显示了两次,第二段代码中,以前我们写if else语句,二者只能执行一个,但是绝对不能执行两个,但是这次两个都执行了。而第三个在执行fork之后,执行的printf死循环语句中两个进程居然不一样。

通过这些奇怪的现象,本质是fork之后,有两个执行流。

 

ps:在vim模式下怎么查看手册?

普通模式下,直接输入man + fork命令就能查看手册,在vim模式下,要在esc模式下,“:” + man +fork 查看命令

同样,如果写了makefile文件,想要在vim下直接编译,就是在esc下“:”再加上该有的命令


当写这样一个进程代码,fork下会执行两次cout语句,第二条语句的父进程就是第一条语句的紫禁城,那么第一条语句的父进程就是bash,7758进程就是7759进程的爹

那么如何理解fork创建子进程?

1.通常我们创建进程就是./cmd和执行命令,那么fork又是一种创建进程的方式。但在操作系统角度,这几种创建进程方式没有差别。

2.当fork创建了进程,系统里就多了一个进程,那么就多了一个与进程相关的内核数据结构+进程的代码和数据的PCB块,内核数据结构就是task_struct,进程的代码和数据是谁呢?在第一个进程执行完有了task_struct和代码+数据,那么子进程的代码和数据从哪来?

 fork创建子进程后,默认情况下,会“继承”父进程的代码和数据,内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct。也就是说fork之后子进程的描述的数据结构(PCB)是跟父进程一样的,里面的代码和数据也是以父进程为模板或者继承父进程。

fork之后,子进程和父进程代码是共享的。那么子进程的代码是不可修改的,父子进程代码只有一份。那么默认情况下,数据也是共享的。在我们的操作系统中,有时候会同时打开很多软件,当一个软件关闭后,不会影响另一个软件,说明进程具有独立性。但是数据也要考虑修改的情况,当父子进程只有读操作时候,他们的数据是共享的,如果要修改一个进程的数据,通过“写时拷贝”来完成数据的独立性。所以写时拷贝是为了维护进程的独立性,让各个进程不被干扰。

但是如果没有写入操作,也就是修改操作的时候,我们是不需要进程写时拷贝,共享一份数据即可。如果只读不写,写时拷贝会造成空间的浪费


 那么我们创建子进程就是为了跟父进程干一样的事情吗?一般是没有意义的,通常父子进程要做不一样的事情。

父子进程一般是靠fork的返回值来完成的!

这样进程中有两种返回值,因为一个id里面有两个结果。但是从前没有出现有两种返回值的情况,但是在fork之后又两种结果返回值。

如何理解有两个返回值?

两个返回值代表两个数据,如果一个函数已经开始执行return,说明函数的核心功能已经执行完了,在上面图中的代码中,fork()执行后,会返回一个id,这也是返回值,先执行父进程的返回值,因为父进程先改变,所以先进行了写时拷贝,也就是说,返回值也是数据,return的时候会进行写入,发生了写时拷贝。这也就证明了return语句也是语句!

如何理解两个返回值的设置

父进程与子进程是1:n,一对多的关系

int main()
{
pid_t id=fork();
if(id==0)//子进程
{
while(true)
{
std::cout<<"I am child:pid:"<<getpid()<<",ppid:"<<getppid()<<std::endl;
sleep(1);
}
}
else if(id>0)//父进程
{
while(1)
{
std::cout<<"I am parent:pid:"<<getpid()<<",ppid:"<<getppid()<<std::endl;
sleep(1);
}
}
else
{

}
sleep(1);
return 0;
}

通过ps axj | grep myproc发现这两个进程在运行

 进程状态

在操作系统中,进程状态的意义是为了方便操作系统快速判断进程,完成特定的功能,比如调度,本质是一种分类。进程状态的信息一般放在task_struct(PCB)中。

为了弄明白正在运行的进程是什么意思,我们需要直到进程的不同状态,一个进程可以有多种状态,在Linux内核里面,进程有时候也叫做任务。

具体状态:

  • R:运行状态。

运行状态的资源不一定占用CPU,有时候会在一个等待队列中,呈现运行状态,等待被调度。这个队列也就是task_struct,描述进程的控制块组成的队列,里面放的都是每一个要执行进程的代码和数据。这时候对进程的调度也就变成了CPU对task_struct的增删查改。

  • S:休眠状态--可中断睡眠--一种等待状态--可中断睡眠
  • D:深度睡眠状态--不可中断睡眠

当我们像完成某种任务的时候,任务条件不具备,比如先从磁盘读入数据,但是磁盘已经满了;想从网络读取数据,网络断了等等这种任务条件不具备,需要进程进行某种等待,需要S或D。

和等待运行队列一样,等待进程会形成一个等待队列,在缺乏条件的时候,这种不可被直接调度的进程,进入等待队列,但是等待队列放的不是代码和数据,而是进程控制块,R状态的队列直接放代码和数据,而阻塞状态队列放PCB,再通过PCB来寻找代码和数据。

我们把,从运行状态(run_queue),放到等待队列中,就叫做挂起等待(阻塞);从等待队列放到运行队列,被CPU调度就叫做唤醒进程。系统中一般存在大量阻塞队列,少量运行队列


那么什么是D不可中断睡眠呢?

当一个进程要往磁盘中写入数据,但是此时磁盘并没有时间来做这个进程,磁盘让进程在内存中等待。此时磁盘对进程说,你等我一会,等我把现在的事情忙完,我就执行你的进程。此时进程在内存中等待,磁盘去做别的事情了。此时操作系统会对进程进行管理,看到有进程什么事情都没有做,系统中的资源本来就不够,进程还在占着茅坑不拉屎,这时OS将等待的进程释放。但是等磁盘昨晚当前进城后,回头找刚才等待的进程,发现刚才等待的进程没有了。因为磁盘运行完上个进程无论成功还是失败,都需要对下一个进程返回一个状态,但是此时进程却没有了。

根据以上描述,为了让等待的进程不被操作系统释放,有了不可中断睡眠D。

所以有两种状态睡眠,可以被杀掉的睡眠S,和不可被杀掉的睡眠D。

  • T 暂停状态  --  使数据彻底不更新
  • t  追踪状态  --  当进入调试阶段如gdb模式下,进程处于追踪状态
  • X 死亡状态

一旦进程进入死亡状态,操作系统要回收它的资源,回收进程资源=进程相关的内核数据结构 + 它的代码和数据,当我们创建进程,会创建描述进程的进程控制块以及它的代码和数据,那么释放的也是释放这两种。

死亡状态很难被查到,就在一瞬间,PCB已经被释放。

  • Z  僵尸状态

辨别退出死亡的原因!给出进程退出的信息,也是数据。

那么当进程退出的时候,它的资源并不是立即被释放,而是先进入僵尸状态,将进程退出的所有原因写进PCB的task_struct描述数据结构中,让系统和父进程进行读取,此时的task_struct被称为僵尸状态。所以正常的死亡状态是Z->X->被释放。

一个进程如果不会收,会让进程一直处于僵尸状态,僵尸状态的进程不释放会占用资源,形成内存泄漏。

如果父进程没有检测或回收进程,该进程就会进入僵尸状态Z。为什么会有子进程都停止了,而父进程不回收它的情况?

下面这段代码当中,先创建子进程,两个进程可同时运行,我们知道进程结果为0的是子进程,结果不为零的是父进程,所以我们可以设置父进程等待50秒运行一次,子进程2秒运行一次,父进程在这50秒的时间内会是等待状态,什么都不干。如果把子进程杀掉,父进程处于等待状态,使得被杀掉的子进程没有被回收,变成了僵尸状态。

如果父进程被回收后,子进程如果想被回收,操作系统会想方设法去回收子进程,但是这样的父进程在等待状态,也没有被回收的情况下,父进程就不会管子进程,操作系统也没有办法回收子进程,这样子进程变成僵尸状态。

int main()
{
pid_t id=fork();
if(id==0)
{
//child
while(true)
{
cout<<"I am child,running!" <<endl;
sleep(2);
}
}
else{
//parent
cout<<"father do nothing!"<<endl;
sleep(50);
}

return 0;
}

但如果子进程还在运行,父进程已经挂掉。这样的话子进程就没有人回收了,这时候子进程就变成孤儿进程,它会被1号进程领养,也就是它的ppid变为1,这个1号进程就是操作系统。所以当孤儿进程被杀掉要被回收的时候,1号操作系统进程就来回收这个孤儿进程。


进程是一直在RS状态之间切换,因为外设访问CPU实在太慢,对于CPU而言,我们外设访问的没有CPU运行快,cpu很大程度上都在等待

如果把一个进程暂停,那么是不是也有暂停状态呢?

那怎们暂停呢?

使用kill -l命令查看,发现18和19两个分别是继续进程和暂停进程

 当运行进程,输入kill -19 +进程号,就可以暂停一个进程,通过ps axj | grep myproc查看进程的pid,输入命令kill -19 进程号,输入后发现进程变为T状态。

再让它继续运行,那么输入kill -18 +进程号,将暂停的进程继续运行,当我们运行再次查看进程号发现,进程号的状态后面没有+了,而且按ctrl+z这样的退出信号也不会退出,这是因为,只要运行了暂停命令,这个进程就在后台运行,那么该如何杀掉进程?

杀掉进程:kill -9 进程号

当我们直接运行一个进程,这样的进程是在前台运行的,这样的进程用ps axj | grep mygrep(mygrep是我写的一个文件)命令后,查看到的进程状态后面是带一个“+”的,典型特征是按什么键都不好使,但是ctrl+C/Z后,进程停止。

 如果在进程运行命令后面加一个取地址,这个叫做后台命令,虽然也在屏幕当中刷新。它的典型表现是可以执行你的命令,但是ctrl+C/Z命令不好使。

[wjy@VM-24-9-centos 330test]$ ./myproc &

 孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要有init进程回收喽
举报

相关推荐

0 条评论