含情脉脉的进程

冯·诺依曼体系结构

一个计算机在工作的时候是怎样的呢?

 我们所认识的计算机都是由一个个的硬件组件组成:

输入设备:键盘、鼠标、摄像头、话筒、磁盘、网卡

中央处理器(CPU):运算器、控制器

输出设备:显示器、声卡、磁盘、网卡

是不是大家通常会觉得计算机是这样设计的: 

ae7b11f01074429ba16d460ddd4f0e3d.png

但其实并不是,计算机是遵循冯·诺依曼体系结构来设计的,增加了个设备:存储器(内存)

f10a2a45c6194d58b1a2052eec7179d5.jpeg

量化概念:

CPU:纳秒

内存:微秒

设备:毫秒

它们基本上的速度差别就是eq?10%5E%7B3%7Deq?10%5E%7B4%7D这些

冯·诺依曼体系是一种架构构成,设备是相互链接的,数据的流动本质是从一个设备传输到另一个设备的过程,数据是要在计算机的体系结构中进行流动的,流动过程中进行数据的加工处理
从一个设备到另一个设备的本质是一种拷贝,数据在设备间拷贝的效率决定了计算机整机的基本效率(主要矛盾)

CPU计算的速度肯定很快

存储金字塔

955b115739e6434ab2d1c3990d2fa262.jpeg

存储结构距离CPU越近,效率越高,成本越高

比如寄存器,速度很快,离CPU近,造价高

计算机的存储架构不是这样就很显而易见咯:

ae7b11f01074429ba16d460ddd4f0e3d.png
根据木桶原理,若是这种体系结构,那太慢了(能盛水取决于最短的木板)
CPU大部分时间不是在算,而是在等(等输入设备把外部数据拷过来,等着把数据拷给输出设备)
计算机的基本效率取决于输入和输出设备,冯·诺依曼架构的设计将计算机从毫秒级别拔高到微秒级别

利用冯·诺依曼体系架构设计计算机快慢就取决于存储器了,存储器是一个巨大的缓存,可以很大程度上提高效率
那假设,我是土豪我有钱,我就用寄存器,我所有的硬件都用寄存器,硬件工程师,你做是不做?

老板大气,这台计算机五百万卖你,但又不是所有人都像你一样有钱
各个体系架构存在是必要的,创新带来范围影响,技术上突破,传播上普及(历史意义)

做个简单的总结

在硬件流动角度:

1.在数据层面,CPU不和外设直接打交道,只和内存打交道
2.外设(输入和输出)的数据不是直接给CPU的,而是先放入内存中

那程序的运行为什么要加载到内存中?

程序=代码+数据,需要被CPU访问、执行,而CPU只会从内存中读取代码和数据(冯·诺依曼体系规定这么做)

程序没有被加载到内存的时候在哪里呢?

是磁盘(外设)上的一个普通二进制文件

还是来举个栗子吧:

已知墨墨酱(IP北京)是一个超绝程序员,她觉得通信是一件很神奇巧妙的事情,某天她在QQ上认识了一位朋友凉白开(IP湖北),正所谓君子之交淡如水,于是墨墨酱打招呼的第一句话是:你好!

那么在这个过程中,消息是怎样流动的呢?

既然对方用电脑,我也用电脑,那么我们两个本质就是两个冯·诺依曼体系(软件玩的再怎么花,硬件必须这样流动)

c4e75b4a58a7417aaf93f980c0158f22.png

操作系统

概念

首先,操作系统是一款软件,是一款进行软硬件资源管理的软件

广义:操作系统内核+操作系统外壳周边程序(给用户提供使用操作系统的方式)

狭义:操作系统的内核

架构

既然操作系统可以管理硬件,那一定是可以被操作系统访问的,每一种硬件的物理特性肯定不同,如果由操作系统直接访问硬件,则一旦硬件发生变化,操作系统就要响应的做出改变,所以增加了操作系统与硬件层中的驱动层(软件,每一种硬件都有自己的驱动层),大部分驱动层都是由厂商自己提供的

dc0279b8797f42a2b633e31d9a2887e7.png

                                                                体系架构的层状划分

在我们外接设备的时候,比如键盘,鼠标,但是当接入电脑的时候会显示驱动程序正在启动,大约等个一两秒才能动

存在意义

操作系统对软硬件资源进行管理(手段),为用户提供一个良好的运行环境(目的)

以人为本,工具

操作系统对下提供基本的管理工作,让各种硬件工作在一个稳定的,高效的,安全的状态当中。(针对计算机,手段)
操作系统对上要提供一个高效的稳定的,安全运行环境(针对用户,目的)

理解

操作系统中的“管理”是什么概念呢?

拿简单的学校来举个例子,墨墨酱是一所学校的校长,校园中存在着很多角色:校长、主任、老师、学生...

我们在完成一件事的时候,离不开两个动作:

1.决策

2.执行

管理者之所以被称作管理者是因为他的任务绝大多数和决策有关

学校里真正的管理者是校长,被管理者是学生,中间夹杂着一层角色:辅导员(可是灿灿,人生是旷野~)虽然校长是管理者,但是我们平常基本上没机会见到他(他不会早上一脚踹开我的房门叫我起床上早八)

好管理不需要管理者和被管理者直接接触,可是不直接接触又是怎样进行管理的呢?

见面不是目的,拿到数据才是目的!

管理的本质不是对人的管理,而是对人身上的数据进行管理!

但是校长见不到学生又是怎样拿取数据的呢?

当然是引进一个中间角色:辅导员

校长关心的是学生,而不是某个人

校长关心学生的成绩,绩点,参加竞赛情况...不关注学生爱吃啥最近谈恋爱了没有...

随着被管理同学的增多,人的信息也变多,校长难于管理

前文说过墨墨酱前身是程序员,会敲代码,给上面的问题作总结就是:关心的属性类别是相同的,属性值不同

struct stu
{
    char name[16];
    char sex;
    int age;
    ...
    int Math;
    int Chinese
    double English
    ...
    double score
    struct stu* next;
}

将表格转化成结构体,这个过程叫:面向对象!

struct stu* zhangsan = new struct stu{...};

对于任何管理,我们都先描述,再组织,上面的由日常校长对学生的管理工作变为对链表的增删查改是完成了对学生管理工作的计算机建模的过程

拿STL来举例,本质是C++的一种容器,容器的本质就是数据结构,对数据结构进行封装,封装的本质描述对象,是对STL的组织分析

Linux理念一切皆文件,C++理念一切皆对象

特定对象直接先描述再组织,那操作系统对于硬件怎么管理捏?

05d4f5be395a4e5995e2ad3ccb19680d.png

还是先描述再组织,对设备的管理变成对链表的增删查改

讲个故事,墨墨酱是一个大学牲,她有一天去银行办理业务,银行的职员很多,有放贷的,有存款的,有和企业对接的,有接待大用户的...

她想要建立自己的小金库,她要存一百块,银行肯定不允许墨墨酱自己存(那像什么话),银行不相信任何人,群众里面也有坏人,银行又必须执行它的任务,发挥它的作用,那银行怎样在保证自身系统安全的情况下为用户提供服务呢?

可以用柜台实现人办理业务,用户把身份证和银行卡递交,交给银行的工作人员办理业务

bcbe73b21f444159962c57dcc65d5173.png

操作系统内部涵盖属性结构,如果用户非法入侵篡改,对操作系统的影响是致命的,操作系统也不相信任何人,但要为用户提供业务,所以它为用户提供软件层(系统调用),不能直接进入,系统调用(操作系统提供的函数)保证用户调用接口传参,操作系统自己进行操作再返回给用户,会让操作系统处于稳定的状态,进程状态就是处理大量的系统调用接口

因为操作系统不信任用户,内核是用C写的,所以就为用户提供了大量的系统调用接口,用成员函数的方式访问类属性(成员函数是类对外提供的接口)

对于银行我想说,银行是一个庞大的机构,它不仅可以做到为普通用户提供业务,还要为特殊用户提供业务,比如老燃已经七老八十了,不识字,想给孙子暖暖存一千块压岁钱,银行职员怎么应对这样的特殊用户呢?

银行为了更好地给老燃这样的人提供服务,银行不仅设置窗口柜台,在大厅也会设有咨询工作人员

288c31b4512c405a8b6938b37846976a.png

老燃去银行历险,大厅工作人员直呼内行,直接帮扶老燃完成所有业务(暖暖也是有钱花了)

87ee6d819e3546ea8e89953b6ba2a8f8.jpeg

(其实是委托算命)大堂经理直说:老燃,我帮你叫号,你写啥资料我可以代写,您歇着就行!

大堂经理和算命说,你,去帮老燃弄后面的事情!

算命接收了这个任务,完成了这个任务,帮助老燃这样的弱势群体也能进行存款取款业务办理

映射到操作系统这边就是,操作系统调用是操作系统提供的接口,有些难以理解,需要会一些知识,不适应大众使用,于是用户操作接口应运而生(为了方便老燃这样的特殊人士也能使用),这些库我们也早就接触过:C/C++标准库

我们一直都在用,之前已经说过,链接有动态链接有静态链接,这条命令可以查询某个可执行程序都用到了哪些库:

ldd 文件名

f0c8e233f74b40eabf43eb047e3f39d0.png

 /lib64/libc.so.6就是传说中的C语言的动态库:

312ad4a3680047c59fc6273b04f398da.png

 其实我和老燃也差不多,历史上并没有直接使用过系统调用,都在用用户操作接口,所有动作都是用库来做的(用C/C++的动态标准库来做)

如果要自己实现一门编程语言,那这三剑客必不可少:语法、标准库、编译器

这样以后就直接用库而不用系统接口的(安装VS的时候一方面在安装编译器,一方面在安装标准库、开发包)

操作系统不同,提供的接口不同(函数名,参数,返回类型...)在用户层若直接使用系统调用接口,那在 Linux下所写的代码只能在Linux下编译运行,无法在Windows下编译运行(由于系统调用接口不同),但是若使用用户操作接口就都能运行(在Windows下打印用printf,Linux下亦然),底层差异上层看不到,用起来就很方便(跨平台性)

这部分可以被称作比较完备的操作系统:

b70ab2665f654644a66fa1e670ec8690.png

进程

 什么叫做进程呢?打开Windows任务管理器(Ctrl+Shift+Esc),这些都是进程:

4901db28c0f84333aa0de873e163dfb0.png

操作系统中,进程可以同时存在非常多,所以操作系统肯定要管理进程(先描述,再组织)

进程

课本概念:程序的一个执行实例,正在执行的程序等

内核观点:担当分配系统资源(CPU时间、内存)的实体

 在这里processbar是一个二进制文件,运行起来就可以被称作一个进程f44a74e9c7df43f78a57012ea297a3df.png

那processbar在没有加载时在哪里呢?

 在磁盘里!描述图解:

9970044059b0441b8475649b92f14daa.png

每个进程创建时都会有一个对应的struct PCB:

struct PCB
{
    //所有属性
    struct PCB* next;
    //内存指针(指向自己的代码和数据)
    //一个进程一个PCB
    //进程=代码、数据+PCB
}

 这样以后对进程的管理就变成了对链表的增删查改

PCB
概念

PCB,江湖人称process control block(进程控制块),就是描述一个进程的结构体,这个数据结构是操作系统内部管理进程的内核数据结构(操作系统的数据结构)

操作系统是软件,使用(开机)的过程就是在加载操作系统,操作系统在没开机的时候也是存在磁盘的二进制文件一枚~开机是讲操作系统加载到内存中的过程,操作系统是计算机启动时加载的第一个软件,进程不仅需要讲可执行程序的代码和数据加载到内存中,还要操作系统malloc结构体PCB出来(一个进程有一个PCB,对进程的管理变为对PCB的管理)

学校管理学生,那怎么证明我是邮专的学生呢?(门禁刷不进来,只有赔笑脸才能刷进来,我已经被邮专开除了)

有人说:那就看在没在校内呗,在校内的肯定是学生啊,保安大叔应如是,保洁阿姨有话说,宿管阿姨呼内行,和人无关,决定这个的是人的属性信息是否在学校的教务管理系统内(学校==内存)

操作系统=教务管理系统

怎么证明是一个进程呢?

学生的属性信息存到教务系统中相当于PCB在操作系统内部,使用指针将PCB连接起来,将PCB管理好相当于将进程管理好了,对进程的管理也就变成了对链表的增删查改(并不是直接将可执行程序加载进来,而是管理PCB)

存在意义

为什么要有PCB捏?

因为OS要对进程进行管理(先描述,再组织)

进程=内核task_struct结构体+程序的代码和数据

Linux是一款操作系统,而PCB是操作系统这个学科下的概念,Linux下PCB(进程控制块)具体的结构是:

struct task_struct
{
    //Linux进程控制块

}

举个栗子吧:墨墨酱是一名大学生,她今年已经大三了,该去实习界闯荡一番了,她去慢手、丑团、饱了么、字节跳动不了一点投递简历,算命、暖暖、燃燃,笙宝也要把简历投给这些大厂,这投递的本质并不是把人交给公司,而是将简历投了过去,而每个人对应的简历就是描述每个人的task_struct,简历放成一摞,面试官一个一个看的过程本质上就是简历在排队,简历在排队也就相当于人在排队

b3a7bc06d8204436b63719fe84a8ecb8.png

调度运行进程本质就是让进程控制块task_struct进行排队

对于进程,进程的代码和数据相当于进程的实体,管理是在管理进程的task_struct(task_struct是进程的简历),对应聘者的操作变成了对简历的增删查改,这就是进程控制块存在的意义

理解

课本上常说:进程是动态运行的,怎么理解这个概念呢?

应聘面试流程:投简历 --> 笔试 --> 一面 --> 二面 --> 三面

持续周期可能2~3周,,并不是人在周转,而是简历在被一二三轮面试官拿到,这个过程叫动态面试,而对于进程来讲,进程一会被放到运行队列里被CPU运行、一会在等待显示器、键盘、磁盘、网络资源,进程动态运行的特点主要体现在CPU或其他设备想要被进程访问执行时,都是以PCB为代表被来回调度运行的,只要进程的task_struct在不同的队列中,进程就可以访问不同的资源(PCB结点被放到不同地方了)

PCB是进程的简历,包含进程的诸多属性,可以来学习下:

task_struct属性
启动

./xxxx运行某个程序本质就是让系统创建进程并运行

061544e868ea4f8c938a80e77f772d13.png

之前学过的命令也是可执行程序,比如上面的这个find是x86-64位下的一个可执行程序 

自己的程序本质上也是x86-64位下的一个可执行程序

a7ddaa22499147308370608baa37e3e2.png

我们自己写的可执行程序、系统的命令都属于可执行文件,在Linux中运行的大部分执行操作本质都是运行进程

那该怎样查看进程呢?

用这条命令:

ps axj

a0cc631b422a4447becd8edc2cdbe303.png

这样是查所有进程,如果想查询特定的某个进程,那就:

 ps axj | grep myprocess

2f52293adf11477eb8d865f9c02c9434.png

 为什么这里还带了grep?

通过管道和关键词检索的方式查出了指定进程,grep本身也是一个进程,所以也被检索出来了

那每一列都是什么含义呢?

这样查,将首行解释列出:

ps ajx | head -1

 两条命令的同时执行连接用&&

ps ajx | head -1 && ps axj | grep myprocess

acfea1e782c14cfa9afa75341c5cf977.png

 每一个进程都要有自己的唯一标识符,叫做进程pid(和学生学号一样),进程PCB唯一,区分用unsigned int pid

如果一个进程想要知道自己的pid该怎么办呢?

struct task_struct被称为内核数据结构,用户是不能直接访问操作系统内部内核数据结构的pid的,那么用户该如何获得进程的pid呢?

需要操作系统为我们提供系统调用

56b6bd69158b4c669d8d21f5bc12a6c4.png

myprocess.c:

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

int main()
{
        pid_t id = getpid();
        while(1)
        {
                printf("I am a process!,pid:%d\n",id);
                sleep(1);
        }
        return 0;
}

运行一下可以看到进程的pid: 

eb8772ebe0db49d8afb2061c03a6bd1a.png

这样可以查到确实是这个进程的pid 

ps axj | head -1 && ps axj | grep myprocess 

 3cca5aba18704c198250edd9e7f621e1.png

在Windows中双击可以启动进程(点开这个.exe文件,我们中国人有自己的打金服,别管我闲的没事养蛊玩)

b0f5f60eb7834e37979d731a329c91d2.jpeg 5f007eecdf8e485eb85f1386ec9653fa.jpeg741e1766337c448cb1227089563255f4.jpeg

e1c1fe92ac684425a41c2a7d9f228422.png c4c1ef08034b4ae1afa64bae83de0075.pngc535d92cfbb94c05a3e92b7b779f20b7.png

a987e6b6116240f9a5f26357a3599f7f.png 97aa3b6557034deabf8e43fc2531290b.png56eb4dfb8bcd4172979a2c3f0484df40.png

b7090b6fede843d59ce2a49a12d357ce.png aa99f6163b48460eb88f94e72d25317c.pnga24691940d284b5482375f3f2ed5d2b7.png

ce229931396d4df98287859297d04357.png4afe5266f2ca4aeba1cbe33a41d1a78d.pngf451a5d9ecde4033bfecdd9ebf8204d5.png

6ec7bffa9d4846a5aa0cf172e339abb7.png 5344609b81c24bce82050cbcfe2c364a.pngb1c23c55c8154cffa8e050ae770c66db.png205d768067d7465da09e55845d41db67.png6a3867ecd25041af8154b3f47a55bc4f.png76459fe646414a1f84f16e3f103c289e.png

22f5b1e3f0e247aeaf24fdcfe5cab706.png37a9c9258d12454a8cd4c5b012cad905.pngba2aa64b416844de97fbff2adcfdaa31.png

 在Linux下启动一个进程就是./执行可执行程序,既然进程已经启动,那就当然可以用ps命令来查找进程,那假如想要终止进程该怎么做呢?

之前说过,有事无事无脑Ctrl+C一下(在用户层面终止进程),乖乖停下哩:

48b200ed26f24febab34c22c9920b343.png

还有一计: 

e4b4667858f04c7b8ef214cdcec184bd.png

每个进程pid唯一,所以可以这样结束进程:

kill -9 18318

01321010e6f44aca9c4a7e266cd974ea.png

 进程创建的代码方式

来看这张图吧:

56b6bd69158b4c669d8d21f5bc12a6c4.png

getpid我知道是啥意思了,那getppid是啥意思呢?

 第一个p:parent,第二个p:process

pid_t getppid(void);
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
        pid_t id = getpid();
        pid_t parent = getppid();
        while(1)
        {
                printf("I am a process!,pid:%d ,ppid:%d\n",id,parent);
                sleep(1);
        }
        return 0;
}

所以它的意思就是获取父进程的id

16a2dccaf1b749d99940ddb4df34f3b1.png

这样查询: 

ps ajx | head -1 && ps axj | grep myprocess

 62047cff0c34401f9c8ccdad01a8de10.png

进程每次启动时pid都不同,这正常吗? 

7b743e349559417ea20d65e063fa29eb.png

那很正常呀,笙宝是一个热爱学习的小男孩,他第一次高考完报了清华的通工专业,但是念了半年感觉不太对劲,于是去找道士switch算了一卦,switch说他命中多水,星盘为火,遇土木则吉,于是笙宝痛定思痛,复读一年终于考入了梦寐以求的清华土木工程,功德圆满,那清华给他两次发的学号不一样不也很正常嘛(进程每一次运行的时候pid不一样) 

我们同样可以看到,每次运行的时候ppid是一样的(父进程都是同一个)那18262是谁呀?

ps ajx | head -1 && ps ajx | grep 18262

查一下捏: 

c5767456c94245d29c575efb2bbbf0b8.png

 Bash就是父进程,命令行解释器,不知道的可以先看看这篇燃冬:

Shell命令及运行原理-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/chestnut_orenge/article/details/137106046实习生==紫禁城(子进程)

创建一个进程是否代表着操作系统内多了一个进程?

是这样的捏,那多了一个进程就相当于多了一份PCB和一套该进程对应的代码和数据

创建一套进程会创建PCB(内核数据结构),可是用户是没有权限对内核数据结构进行增删查改的,用户不能直接创建个task_struct,操作系统需要为用户提供系统调用!

浅查一下呢?

man fork

f1b0c2e82ed14bb09e3a95c358ceedbc.png

fef05b665e0e4cdbb075677e4ee3c33c.png

98d3baee9f504fda996d25bfb96d3e9f.png

可以发现它不仅有叉子的意思,还有岔路,分支的意思,它作用的解释就是:

fork - create a child process

创建一个紫禁城 

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

int main()
{
        printf("process is running,only me!\n");
        sleep(3);
        fork();
        printf("hello world\n");
        sleep(5);
        return 0;
}

 这是一个监控小脚本:

while :; do ps ajx | head -1 && ps axj | grep myprocess | grep -v grep; sleep 1; done

这样可以实时监控进程咯: 

ba16d9c1e0214b13a0c305f595ad4873.png

刚开始启动的进程:19137一定是父进程, 通过调用fork看到了两个执行流,查到了两个不同的进程,有两个不同的pid(19156就是传说中的紫禁城),在fork之后,父子代码共享:创建一个进程,本质是系统中多了一个进程,多了一个进程就是多了一个task_struct进程控制块,多了的进程还要有自己的代码和数据,那紫禁城的代码和数据从哪来呢?父进程的代码和数据是从磁盘加载进来的,在默认情况下,紫禁城继承父进程的代码和数据(紫禁城没有代码和数据,只能继承,只有自己的task_struct)

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

int main()
{
        printf("process is running,only me!,pid:%d\n",getpid());
        sleep(3);
        fork();
        printf("hello world,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
        return 0;
}

a9cfd92b4e8046b6ab4dbeef08a3a176.png

eb3ff06080524d4f9ba4b8f7facfe914.png

可以看到执行进程一个是父,一个是子 

我们可以通过这样的的代码来查看进程运行状况

我们为何执着于创建紫禁城?父进程一个人不足以吗?

主要是因为我们想让紫禁城执行和父进程不一样的代码,这方面还需要来学一下fork的返回值,,,

pid_t fork(void);

aba85c55506c436988b983180d76d5c5.png

9019903565574c2fb460b1066237ccf8.png

c228e8b3287d43fdba4fd19001534232.png

如果fork函数成功了,则它会返回紫禁城的pid给父进程,返回0给紫禁城,如果创建失败则返回-1,错误码被设置,也就是说fork会返回两次,每次不同的返回值

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

int main()
{
    printf("process is running,only me!,pid:%d\n", getpid());
    sleep(3);
    pid_t id = fork();
    if (id == -1) return 1;
    else if (id == 0)
    {
        //child
        while (1)
        {
            printf("id:%d,I am child process, pid: % d, ppid : % d\n",id,getpid(),getppid());
                sleep(1);
        }

    }
    else
    {
        //parent
        while (1)
        {
            printf("id:%d,I am parent process,pid:%d,ppid:%d\n",id, getpid(), getppid());
            sleep(2);
        }

    }
    return 0;
}

1df65772300c46edb586ea0ff92da9a8.png

b66b48d2298c4738948816d95c5c0e7c.png

浅浅来解释下:fork之后,父子进程的代码都是共享的

对于id=0是给紫禁城的,id=紫禁城pid是给父进程的,因为是分支结构,所以进入到两个分支,实现父子进程同时跑,但是你看代码,在之前的学习中肯定是不可能出现两个while循环同时跑的情况的(以前学的全部都是单进程,多进程情况也是适用的)再想想,同一个id值,为什么又>0又=0啊,教练教教我?

fork会有两个返回值,返回两次,怎么理解啊?

首先fork()是一个函数,只不过是由操作系统(OS)提供的,会有自己的实现逻辑,会有return id,那当我们的函数执行到return的时候,是否意味着函数的核心工作已经完成?

对啊!

我们今天要执行的函数是fork(),紫禁城已经存在了,而且已经可以被调度了

那return是代码么?

肯定是啊!

fork内部前半部分由父进程创建紫禁城,执行到return的时候已经有两个进程了,代码共享,父进程执行一次return,紫禁城执行一次return

孩子们,这并不奇怪

tips:kill掉紫禁城不会影响到父进程

acac47caee144d2fb07663432907c60f.png

b439b5ad4aef4801a5c6cb2042adf1e5.png

那同一个id即>0又=0是怎么会事呢?

紫禁城在创建的时候要有自己的PCB,这个PCB在数据结构层面与父进程的PCB是并列的,在逻辑上是存在父子关系的,父进程的代码和数据都是从磁盘上加载进来的,紫禁城继承父进程的代码和数据,紫禁城肯定也要用父进程=的数据对吧

进程一定要做到:

进程具有独立性(哪怕是父子的亲密关系)

进程 = 内核数据结构task_struct + 代码(只读,不可被修改)和数据

数据在原则上要分开独立,出于对效率的考量,父子的id有独立的数据的,只不过是共同用了id这个变量名,内容是不一样的

一次创建多个进程
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<unistd.h>

void RunChild()
{
    while (1)
    {
        printf("I am parent, pid :%d,ppid: %d\n", getpid(), getppid());
        sleep(1);
    }
}
int main()
{
    const int num = 5;
    int i = 0;
    for (i = 0; i < num; i++)
    {
        pid_t id = fork();
        if (id == 0)      //因为父进程的id不等于0,所以直接跳过判断执行下次循环,创建紫禁城
        {
            RunChild();
        }
        sleep(1);
    }
    while (1)
    {
        sleep(1);
        printf("I am parent, pid :%d,ppid: %d\n", getpid(), getppid());
    }
    return 0;
}

2845c09611274509b5329282a6ab1cea.png

ffc31476099942168f501859aa948be4.png

进程创建可以用代码的方式:fork(),而不是每次都需要./来启动进程

task_struct内容

task_struct

标示符: 描述本进程的唯一标示符,用来区别其他进程

状态: 任务状态,退出代码,退出信号等

优先级: 相对于其他进程的优先级

程序计数器: 程序中即将被执行的下一条指令的地址

内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]

I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

其他信息

优先级:哪个进程先运行 (食堂排队打饭就是在确定打饭的优先级,确定进程的优先级是在给进程的task_struct排队)

可以在内核源代码里找到组织进程,所有运行在系统里的进程都以task_struct链表的形式存在内核里

查看进程

除了ps命令,进程还可以通过/proc系统文件夹查看(proc是根目录下的一个文件夹):

fcb796d54a694614ba17000966bc63a2.png

c693e9732c49443f883b58450c5ce25f.png

2d8132de5b9d4c268cf18ee6293cefe3.png

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
    pid_t id = getpid();
    pid_t parent = getppid();
    while (1)
    {
        printf("I am a process!,pid:%d ,ppid:%d\n", id, parent);
        sleep(1);
    }
    return 0;
}

eaed3c10f3a14eac9e84ef7952f04ccc.png

要获取PID为1的进程信息,你需要查看/proc/1这个文件夹,其中目录是以进程的pid命名的: 

ll /proc/29129

49f8063418294c17aff810015454fc5c.png

其中比较重要的一个属性是exe :

6ea1aae2d7764f87b31e2f751eb48454.png

 也就是说这个exe是由绿色路径下的可执行程序加载出来的

我们假如把可执行程序干掉,那程序也还能跑,这是为什么捏?

因为如果文件要被进程调度,那在内存中已经有一份了,所以把磁盘上的可执行程序删掉了进程也照样可以跑(内存中有,原则上能跑)

be9543520caf47c490d940882013e416.png

3d10691959c240c68c3f3f94dd47bf5a.png

7e370a0151634b919365f071757917ca.png

万一程序非常大就够呛了,电脑内存假设只有8G,可执行程序有16G,如果删掉了,进程可能运行着运行着就出问题了(事已至此,就讲一下暖暖和劫的相爱相杀吧,劫在早期出的时候是要花钱买的(好像八十多?),暖暖作为一个游戏重度爱好者(大收藏家)肯定是不会错过这样一个炫酷的游戏,于是激情购入),可是他的电脑空间不太够,于是他买了个固态硬盘以获取足够大的空间下载这个3A巨作,但是他下好之后进入游戏,发现逐帧播放,游戏卡成PPT,于是他又买了个内存条,百事百不灵,只好作罢,为了一个八十块的游戏花了八百块,暖暖暖暖的在暖群)

27b7fb73d0494f6381a6f80f378b3ab8.pngfb48eb2a792844b59268bc37de63ae24.png

 进程的PCB会记录自己对应可执行程序的路径,那这个cwd是什么呢?

9bbf2131b3df4b77b883fcac866661d6.png

current work dir -- 进程的当前工作路径

每个进程在启动时,会记录自己在哪个路径下启动  ---  进程的当前路径

在前面学C的时候,文件操作那(呜呜,想鹏哥了)

fopen("log.txt","w");

都是进程的代码在执行 cwd/log.txt 

这样它会在当前路径下新建,什么是“当前路径”?,看个例子:

#include<stdio.h>
int main()
{
        FILE *fp = fopen("log.txt","w");
        (void)fp;   //ignore warnning   

        return 0;
}

d25049ad03034a5cb3da78eb83ceb3b8.png

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

int main()
{
        chdir("/root/ice");

        FILE *fp = fopen("log.txt","w");
        (void)fp;   //ignore warnning   
        fclose(fp);

        while(1)
        {
                printf("I am a process,pid:%d\n",getpid());
                sleep(1);
        }
        return 0;
}

可以发现路径发生改变了: 

6cb0adb02d2e4eb1bdd86872c3f64b2f.png

 也确实在我们指定的目录下新建文件了:

6e8ff49902a049288cefc65b22841a87.png

 进程状态

每个进程都要有自己的状态

Linux进程状态

进程状态是task_struct内部的一个属性:

#define RUN 1
#define SLEEP 2
#define STOP 3

struct task_struct
{
    //内部属性
    int status;

}

struct task_struct process1;
process1.status = RUN;

Linux改变一个进程的状态就是在改变task_struct的内部属性 (定义出的标志位,为了表示进程的状态)

Linux内核中对于状态有什么定义呢?

可以看看kernel的源码:

/*
* 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运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里

 S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束

T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

怎么验证进程有着这样那样的状态呢?

ps aux / ps axj 命令

6dcec89cc0894c04b248969aaab3174c.png

 首先先看一下关于makefile的骚操作:

myprocess:myprocess.c
        gcc -o $@ $^
.PHONY:clean
clean:
        rm -f myprocess

这是一段已经写好的makefile,怎样可以快速copy呢?

在底行模式下,我们可以这样干一键替换 

%s/myprocess/testStatus/           

后者替换前者 

2c3d52300dd641e1a5b4cced0735834a.png

再把依赖文件改一下就好哩:

testStatus:testStatus.c
        gcc -o $@ $^
.PHONY:clean
clean:
        rm -f testStatus
while :; do ps ajx | head -1 && ps ajx | grep testStatus | grep -v grep; sleep 1; done

 我们可以发现这个进程一直在狂刷:

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

int main()
{
        while(1)
        {
                printf("I am a process\n,pid:%d\n",getpid());
        }
        return 0;
}

fbb142bc0d424a9ab833972677c96958.png

 但是一看状态怎么他喵的是sleep啊?fb221b9b667d49c3aa2243879a0b0727.png

我把源码稍加改动:

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

int main()
{
        while(1)
        {
        //      printf("I am a process\n,pid:%d\n",getpid());
        }
        return 0;
}

 现在检测可以发现是正常运行的哩:8daa2378013c4155a2a566c596dba94c.png

R表示进程正在运行,可是打印又是什么很邪恶的操作吗?凭什么说我在睡觉???

 S表示休眠状态,即进程什么都没做

首先我们明确一点:printf打印是往哪打?

往显示器上打印对吧,程序是在远隔万里的云服务器上跑的,但是打印的结果却返回到了近在咫尺的显示器,根据冯·诺依曼体系架构,显示器是一个外设,所以CPU在跑的时候,一直要把数据写到内存里,再刷新到外设上,那我能保证每次打印的时候显示器都是就绪的么?

程序是CPU在跑,CPU的速度可比显示器快多了,所以进程在被调度时需要访问显示器资源,大部分时间都在等待设备资源就绪,只要没就绪,就处于S状态,CPU执行(ns),打印(ms),所以我们在查询时可能查到的很多都是处于等待状态而并非运行状态,这也是为什么把printf注释掉之后就一直是R状态(可想而知差距有多大才会在刷屏的时候依旧查到很多S)

休眠状态就是进程在等待资源就绪的过程

我们如何真正的看到一个资源在休眠呢?

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

int main()
{
        while(1)
        {
                sleep(10);
                printf("I am a process\n,pid:%d\n",getpid());
        }
        return 0;
}

 上面的进程就是在休眠,使用Ctrl+C可以把进程终止09d87683b8d143a08295952a57c2b15b.png

所以S是可以被中断的睡眠 ,这是让进程在后端运行:

 ./testStatus &

 6d2c418dbedf4988b3c271590d8aa318.png

在后端运行的进程休眠S则不带+了 ,带不带+就是区分进程在前台和后台运行,在后台运行的进程是没法Ctrl+C的,只好干掉哩:

kill -9 2988

我们的进程有不同的状态,有一个状态叫T即停止状态,很有意思的点就在于我们可以通过一个进程控制其他进程:

kill -l

 b2050ac5df8d4b25a523f54394910be3.png

这是很多信号,我们曾经用过的信号9就是杀掉进程,使用kill可以向指定信号发信号,信号就是数字,发信号时也有自己对应的名字(大写的,数字代号----宏!),可以看到成功将进程终止了

33fbf522d1c145ae8471f5176930781b.png

T:让进程暂停,等待被进一步唤醒

19是将进程暂停,18则是继续:

b7dc0573351d419ea6a228d578fc126b.png

 其实我们之前也做过让进程暂停的操作,这个操作叫做调试(在断点处不就停下来了吗,悟了没)

为了让进程可以被调试,修改下makefile文档:

myprocess:myprocess.c
        gcc -o $@ $^ -g
.PHONY:clean
clean:
        rm -f myprocess

浅查一下吧:

readelf -S myprocess | grep  debug

 db890b8d3f3b439dba345e224c70df5e.png

 我们开启监控会发现查不到:

a18e824aa2544c3c9269c294ae9636dc.png

这个正在跑的是gdb,并不是我的进程(相关操作移步我的另一篇)

炫酷gdb-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/chestnut_orenge/article/details/138551058 看看怎么个事:

ps ajx | grep myprocess

 d0e4fb9b15554649b718a6502026fe37.png

可恶!明明查得到!!!大骗子

 奥,原来我只是开启了调试,并没有让进程跑起来啊

在第十行处打个断点再让进程跑起来:

65b5fdab102b46eab28b15def21fd2a5.png

 惊奇发现S+爆改t

t表示当前进程因被追踪而暂停,遇到断点进程即暂停

D状态是Linux系统中比较特有的一种进程状态,名为磁盘暂停状态

操作系统在开机时就在内存中存在着,即开机时操作系统就被从磁盘加载到内存

假设这样的场景:内存中1GB的数据要存到磁盘中(把数据交给外设)

进程在等外设写数据,所以当前的进程就处于一个S状态(休眠),等待写入完毕

Linux操作系统有权利杀掉进程来释放空间(内存严重不足,杀鸡儆猴以儆效尤)

磁盘忙完一圈回来想反馈发现进程被干掉了,啊?不是哥们?

但又不止有一个进程等着磁盘帮忙写入,就造成1GB的数据丢失问题(如果这1GB数据是银行里记录的一天内用户的转账记录,那事可就大发咯)

那锅该甩给谁?

操作系统、进程、磁盘。。。谁有问题?

进程:“老大呜呜呜,我是受害者呜呜呜,我在那正工作呢突然就有个比我官大的不分青红皂白就把我干掉了呜呜呜”

操作系统:“进程你还放上洋屁装起可怜了,我要是完蛋了,我们大家伙都得玩完,老大都说了我作为操作系统,在极端条件下有资格杀掉进程,要是我挂了丢10GB数据这个锅你背得起吗?!”

磁盘:“老大,说白了我就是个送外卖跑腿的,人家让我干嘛我干嘛,只是写入失败了,是硬件出错误了,这总不能怪我吧。。。”

为了防止后续问题再发生,问题的根源就在于操作系统杀掉进程的时候是毫无类别的去杀,想杀就杀,但是这种在写入关键数据的进程是不可以被杀掉的!

那也不能怪操作系统啊,那长得都一样,它还那么大块头,不删留着过年吗?

所以此后规定操作系统执行这样的法则:凡是在进行数据IO的进程一律不允许删!

在传数据的时候,进程要变为D状态:不可被杀!

D状态就是一种睡眠状态:深度睡眠:不可中断睡眠

那怎样结束D状态呢?

肯定可以让它自己醒过来呀!

还可以重启,如果重启都不管用就只能断电了(重启有时也会将数据向磁盘内刷新,如果不成功卡死就只能断电了)

僵尸进程和孤儿进程

进程退出的时候不是干巴巴的退出,是要把退出信息保存到进程的PCB中的,如果没有人读取PCB中进程退出的消息,那进程就一直不释放(代码和数据释放掉,PCB一直存在,直到等待,否则一直处于僵尸状态)当等待时,进程的状态才会转为X,进而将空间全部释放(刑事案件:封锁现场

-->法医验尸-->联系家属火化尸体),所以一个进程退出的时候并不会直接退出,而是会先处于一个僵尸状态,如果父进程不对其进行回收,那么对应的进程将一直处于僵尸状态

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //child
        int cnt = 5;
        while (cnt)
        {
            printf("I am a child,cnt:%d,pid:%d\n", cnt, getpid());
            sleep(1);
            cnt--;
        }
    }
    else
    {
        //parent
        while (1)
        {
            printf("I am a parent,running always!,pid:%d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

上面的进程退出后,紫禁城将处于僵尸状态,监控一下发现确实是这样:

3fe916c640e04433847d9d6f987221c6.png

Z状态表示已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程进行读取(要让父进程知道他为什么退出),如果没有父进程读取,僵尸进程会一直存在

进程 = 内核数据结构task_struct(一直存在,可能会造成内存泄漏) + 进程的代码和数据(被释放)

僵死状态(Zombies)是一个比较特殊的状态

当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码

所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

kill是不能干掉僵尸进程的,它已经死了,你这个顶多算鞭尸

谈完僵尸进程,谈谈孤儿进程:

 如果父进程先退出,我们把子进程叫做孤儿进程,孤儿进程一般都是会被1号进程(OS本身)进行领养的

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

int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                //child
                while(1)
                {
                        printf("I am a child,pid:%d\n",getpid());
                        sleep(1);
                }
        }
        else
        {
                //parent
                int cnt = 5;
                while(cnt--)
                {
                        printf("I am a parent,cnt:%d pid:%d\n",cnt,getpid());
                        sleep(1);
                }
        }         
        return 0;
}

e4e94bfe8f9d4251a59fc08ac7183b12.png

 孤儿进程为什么要被OS领养?

因为我们依旧要保证紫禁城正常被回收

可是教练,,,我之前敲代码也不关心内存泄漏(僵尸进程啊),这东西,,九九成稀罕物

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

int main()
{
        int cnt = 5;
        while(cnt)
        {
                sleep(1);
                printf("I am a process,pid:%d\n",getpid());
                cnt--;
        }
        return 0;
}

我们会发现,这也不异常啊,不是很快自己就退下了?7a9a92b97df647c591a711a6a616e429.png

 主要是因为,命令行中启动的所有进程的父进程是Bash,Bash会自动回收新进程的僵尸问题

进程的阻塞、挂起、运行

我们操作系统讲的进程状态也不是这样式的呀。。。

其中,终止状态==Z、X

操作系统这门学科是所有操作系统的共性(指导思想)

Linux操作系统:具体实现

运行状态:进程在运行时一定是在CPU上运行

进程=task_struct+进程的代码和数据

​
struct PCB
{
    //所有属性
    struct PCB* next;
    //内存指针(指向自己的代码和数据)
    //一个进程一个PCB
    //进程=代码、数据+PCB
}

每一个进程都要有自己的task_struct,采用链表的方式,每一个进程的task_struct指向自己的代码和数据

有很多进程想在CPU上运行,一个CPU配套一套运行队列,运行队列包含对应的指针,对应的进程的task_struct被维护到了CPU的运行队列中(寄存器里),进程在运行队列中,它的状态就是R状态

真是难评,这篇文章见证了从留连到空亡,但是现在还是留连

就绪状态和运行状态可以大致被划分到一起,“我准备好了,我准备好了”就是说可以随时被调度

一个进程一旦持有CPU,会一直运行到这个进程结束么?

并不会,因为它是基于时间片进行轮转调度的,每个进程的时间片结束如果进程没有运行完的话会被操作系统从CPU上剥离下来

这是让多个进程以切换的方式进行调度,在一个时间段内得以推进代码,这个过程就叫做并发

Linux不是这样调度的(上面的只是调度算法的一种,比较简单)

任何时刻,都有多个进程在同时运行,我们叫做并行

阻塞态是一种比较重要的状态,重在理解

举个栗子:在C阶段的时候,scanf输入的话如果不给输入它会在那等着,这个状态怎么说呢?

#include<stdio.h>
int main()
{
    int a = 0;
    scanf("%d", &a);
    printf("%d", a);
    return 0;
}

进程阻塞在这里等待,等待键盘资源是否就绪,键盘上有没有被用户按下的按键,按键数据交给进程

操作系统是软硬件资源的管理者(进程本身是软件)

操作系统是如何对硬件进行管理的呢?

#define KEY_BOARD 1
#define SCREEN 2
struct device
{
	int type;
	int status;
	//其他属性
	struct device* next;
	
};

 等待意味着进程并没有被调度,即没有在运行队列中

#define KEY_BOARD 1
#define SCREEN 2
struct device
{
	int type;
	int status;
	//其他属性
	struct device* next;
	task_struct* wait_queue;
};

 对设备的管理变为对链表的管理、从键盘没检测到数据的时候,进程被操作系统从CPU剥离下来放到磁盘的等待队列中,直接从运行状态转变为阻塞状态

不是只有CPU有运行队列,各种设备也有自己的wait_queue

阻塞和运行的状态变化,往往伴随着PCB被连入不同的队列中,入队列的不是进程的代码和数据,而是进程的task_struct

挂起态

磁盘里有个分区叫swap,一般大小是内存的1.5倍或者和内存一样大,内存中有很多进程(task_struct和代码及数据),有的时候OS内存十分吃紧,一个进程处于阻塞态就意味着当前进程不会被访问,代码和数据也并不会被访问,所以此时内存中的代码和数据就被换出到磁盘的swap分区中,会腾出可观的内存,当进程需要被进行则再进行换入的过程,这个过程进程还存在,进程的task_struct还在内存中,这个状态被称作阻塞挂起态,可以帮助操作系统更合理的使用内存资源

但频繁的换入换出会导致效率问题,进程调度周期变长(用时间换空间)

fdisk是一个工具,用来进行磁盘区域的划分和写入文件系统

CPu内部有非常多的寄存器,啊,其实可以问问:

当然是问chat,,, 

函数调用时候的返回值是如何返回的呢?

临时变量具有临时性,它的值通过寄存器返回

int add(int a,int b)
{
    int c = a + b;
    return c;
} 

当进程被CPU调度时,寄存器会保存进程的临时数据

讲个小故事来描述这个过程吧,暖暖是一名清澈且愚蠢的大学生(样图如下:)

 

他从小就有个当兵的梦想,想要报效祖国

 

学校会和部队合作,所以学生有机会当上一两年的兵

暖暖在大二的时候被应征入伍了,暖暖很开心,去宿舍告诉舍友这个喜事,可以说是奔走相告了

于是暖暖背井离乡,头也不回的走掉了

暖暖在当了两年兵之后回来发现自己被学校开除学籍了

这是为什么捏?

因为暖暖没有打招呼(不是和舍友打过招呼了吗),可是学校布吉岛啊,它以为暖暖出去玩,一玩玩两年,挂了四十多门

成功从大一进阶到高三了,喜事

 后来药师墨给了暖一杯忘情水,换他一生不流泪,有了时光倒流的机会

暖暖痛定思痛,不再草率挥挥衣袖拍拍屁股走人,而是认真的打招呼,和导员说向学校申请保留学籍,暖暖处于的状态是休学的状态,后来回校暖暖该上课上课该吃饭吃饭,但到期末了发现连自己的桌子都没有。。。

暖暖一拍脑门:我靠,回来忘跟学校打声招呼了,我糊涂啊!

他总算学尖了,和导员说申请恢复就读状态,不做期末joker

申请休学保留学籍:保存上下文

申请恢复就读状态:恢复上下文

暖暖:进程                   学校:CPU

CPU所有寄存器中的临时数据,叫做进程的上下文,进程切换最重要的事实上下文数据的保存和恢复

CPU内的寄存器:寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套

但是CPU内部的数据可以有多套,有几个进程就有几套和该进程对应的上下文数据

寄存器≠寄存器的内容

  • 53
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值