认识Linux -- 进程概念

一 冯诺依曼体系结构

我们常⻅的计算机,如笔记本。我们不常⻅的计算机,如服务器,⼤部分都遵守冯诺依曼体系。


截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成:

1 输⼊单元:包括键盘, ⿏标,扫描仪, 写板等
2 中央处理器(CPU):含有运算器和控制器等
3 输出单元:显⽰器,打印机等
关于冯诺依曼,必须强调⼏点:
1 这⾥的存储器指的是内存。
2 不考虑缓存情况,这⾥的CPU能且只能对内存进⾏读写,不能访问外设(输⼊或输出设备)(数据层⾯)。
3 外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。
4 ⼀句话,所有设备都只能直接和内存打交道。

二 操作系统(Operating System)

2.1 概念

任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

1 内核(进程管理,内存管理,⽂件管理,驱动管理)
2 其他程序(例如函数库,shell程序等等)

2.2 设计OS的目的

1 对下,与硬件交互,管理所有的软硬件资源
2 对上,为⽤⼾程序(应⽤程序)提供⼀个良好的执⾏环境

2.3 核心功能

在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的"搞管理"的软件

2.4 理解 "管理"

1.管理的例⼦ - 学⽣,辅导员,校⻓
2.描述被管理对象
3.组织被管理对象


总结:
计算机管理硬件:

  1. 描述起来,⽤struct结构体
  2. 组织起来,⽤链表或其他⾼效的数据结构

2.5 系统调用和库函数概念

1.在开发⻆度,操作系统对外会表现为⼀个整体,但是会暴露⾃⼰的部分接⼝,供上层开发使⽤,这部分由操作系统提供的接⼝,叫做系统调⽤。
2.系统调⽤在使⽤上,功能⽐较基础,对⽤⼾的要求相对也⽐较⾼,所以,有⼼的开发者可以对部分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤⼾或者开发者进⾏⼆次开发。
承上启下
那在还没有学习进程之前,就问⼤家,操作系统是怎么管理进⾏进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!

三 进程

基本概念:

1.一个已经加载到内存中的程序,叫做进程

2.正在运行的程序,叫做进程

理解:

1.一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程

2.操作系统必须将进程管理起来--先描述,再组织

3.任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象 --PCB(process ctrl block)进程控制块.PCB就是进程属性的集合,也是认识和区分进程的依据,Linux中本质是一个struct结构体来存储进程相关属性信息.存在于操作系统中被维护
进程=内核PCB数据结构对象+自己的代码和数据

有PCB才能称为进程,光有代码和数据不算.就比如在学校要有学生信息才算学生,单单人在学校没有学生信息不能称作学生,比如学校食堂的工作人员。

操作系统中只需对PCB进行管理,PCB中具有字段帮助操作系统找到对应的代码和数据来执行,也就是具有相关的指针信息.在操作系统中对进程进行管理,变成了对单链表进行增删查改的管理.

3.1 基本概念与基本操作

3.1.1 描述进程

Linux操作系统下的PCB是task_struct,是PCB的一种,不同操作系统间叫法不同.

task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。这就是通过属性创建一个对象的过程,也就是面向对象的过程

task_ struct属性分类:

1.标示符:

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

2.状态:

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

3.优先级:

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

4.程序计数器:

程序中即将被执行的下一条指令的地址。

5.内存指针:

包括程序代码和进程相关数据的指针,还有和其他进程共享的内存

6.存块的指针:

指针变量存储的是某块连续内存的起始地址,通过该地址可遍历或修改整个内存块的内容(如数组、动态分配的内存)。

7.上下文数据:

进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

8.I/O状态信息:

包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

9.记账信息:

可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。每个进程的处理时间基本要求一致这是判断cpu优劣的一个标准

3.1.2 Linux 进程管理

task_struct结构体中里面包含进程的所有属性,要管理好进程只要管理好task_struct结构体

task_struct最基本组织进程的方式是采用双向链表组织的,但不仅仅是双向链表,因为运行多进程运行,可能采取多种数据结构共同管理.

它要处理什么工作取决于放在哪种数据结构里,数据结构底层对应着不同的算法,不同算法又对应着不同的应用场景

3.1.3 查看进程

ps指令:查看进程状态

作用是显示进程信息的表头

a:显示所有终端上的进程。j:以作业格式显示进程信息。x:显示没有控制终端的进程。

head -1:显示前 1 行输出。

各列含义:都作为属性存储在PCB中(task_struct)

PPID:父进程 ID(Parent Process ID)。

PID:进程 ID(Process ID)。

PGID:进程组 ID(Process Group ID)。

SID:会话 ID(Session ID)。

TTY:进程所属的终端(Teletypewriter)。

TPGID:前台进程组 ID(Terminal Process Group ID)。

STAT:进程状态(Process State)。

UID:用户 ID(User ID)。

TIME:进程占用的 CPU 时间(CPU Time)。

COMMAND:进程的命令或程序名(Command)。

指令ps可以查到所有进程信息,本质是遍历双向链表将所有属性信息格式化打印出来

可以两条指令连续执行,先打印进程信息表头,再利用grep来筛选进程信息,中间&&说明两条指令都必须执行成功,也可以用";"分号来代替

所有指令会加载到内存中以进程的方式去运行,只不过这些指令运行的很快运行完直接被调度;

而grep是用来关键字过滤的,先把自己变成进程运行起来再通过执行代码进行过滤,但grep关键字里本来就有proc,所以在过滤所有进程时把自己即proc也带上了,这就是最后一行打印的原因,侧面证明了所有指令运行时都是进程,若不想显示 ,grep proc | grep -v grep反向过滤指令即可
kill使用方法:

ls /proc指令在系统文件夹查看进程信

关机之后上面的进程信息会清空,但开机之后操作系统会自动帮忙创建这些目录或文件.

这上面的所有信息是Linux操作系统用文件系统的方式把内存中的进程信息可视化出来了上面的数据全是内存级的

白色字体都是普通文件,蓝色字体是目录,目录的特点全部按数字定义,数字代表每个进程的PID,所以所有在系统中默认运行的进程会在/proc目录中创建一个以PID命名的文件夹或目录,保存了该进程的大部分属性

可以通过上述proc进程的PID来查看目录

3.1.4 通过系统调用获取进程标示符

获取自己的PID

是一个系统调用接口,可用man 2 getpid在手册查看,谁调用我就获取谁

可打印出自己的进程pid,进程pid只在运行期间有效,重新启动进程时可能发生变化

获取PPID

-bash

当我们运行一个进程时,命令行解释器会把当前指令解释成bash的子进程,由子进程执行对应的命令,而一旦这个子进程出现问题并不影响我们的bash进程.

1.当我们在命令行中输入各种指令时执行的都是bash的子进程,子进程的ppid是bash的pid

2.重新登录时系统会重新创建一个bash进程,即命令行解释器的进程帮我们打印对话框

3.bash进程只负责具体命令行解释,具体执行出问题只会影响其子进程

3.1.5 fork()

在调用fork函数之后,会生成子进程,返回两个返回值,那为什么会有两个返回值呢?函数不是只能返回一个值吗?

我们要知道的是,一个函数确实只能返回一个值,但是对于创建子进程来说,创建子进程之后那么之后的代码将会有俩个进程一起执行,那么为了更好的管理这创建的子进程,那么就需要依照他的返回值来进行管理,那么fork的返回值一个就是子进程的pid给父进程,另一个返回值为0,代表这是子进程,那么这样就能很容易的区分父子进程了,从而更好管理。

关于fork具体的用法,你可以使用man fork来获取更多的解答。

通过以下代码来验证返回值问题

整个程序执行过程如下,bash在命令行中执行./可执行文件,可执行文件像普通进程一样被创建出来,系统为该进程分配pid,后执行代码到fork处,再由fork一分为二为父子进程,父进程就为它本身,子进程为它的新分支,由单进程变为多进程.

理解fork函数:

1.fork做了什么事情和目的

已知进程=内核数据结构+代码和数据,fork之后创建了子进程,所以也要创建一份内核数据结构PCB即task_struct,其基本属性复制于父进程可能会修改其中一部分,但是子进程没有自己的代码和数据所以注定只能和父进程共享,编译后运行过程中代码不能修改变量和数据可以.

创建子进程就是为了让父与子执行不同的代码块所以有不同的返回值

2.为什么fork要给子进程返回0,给父进程返回子进程pid?

返回不同的返回值是为了区分让不同的执行流执行不同的代码块,一般而言fork之后的代码父子共享;一个父进程可以有多个子进程,而每个子进程只有一个父进程,所以为了让父进程区分并控制每一个子进程,就需要给父进程返回每个子进程的pid

3.一个函数是如何做到返回两次的?

fork是一个函数,它的实现一定存在操作系统内部,变量的创建会在调用进入函数内部中创建,按执行流从上往下执行,一般函数体中返回语句在最后执行到该句时函数的核心工作已完成,fork函数中也就是创建子进程的工作已成,cpu

可以调度父子进程,而fork之后父子进程代码共享所以return语句被共享,父子进程各会执行一次所以有了不同的返回值

4.了解子进程的写实拷贝

首先了解任何平台,进程在运行时是具有独立性的.子进程是没有自己的代码和数据的,在创建子进程的pcb后会指向父进程的代码和数据,代码可以被共享因为运行期间不能被修改,父子进程都只是进行读写工作,数据不能被共享因为可以被修改会影响进程的独立性.因此会给子进程拷贝一份父进程的数据,但也不会完全复制过去因为有的数据子进程根本不会访问这样白白浪费了操作系统的空间,所以只在子进程需要修改数据时单独给拷贝一份在复制品上修改,什么时候修改什么时候拷贝,要多少拷贝多少,这就叫写时拷贝

5.一个变量怎么会有不同的内容?

在调用fork函数时执行return语句时,返回的时候就是在写入数据,写入的是父进程的数据,由于return是共享代码,子进程也会执行,此时为了确保父子进程的独立性就会发生写时拷贝,单独为子进程开辟一个新变量新空间进行数据写入;此时id这个变量在父子进程中都存在为两个变量,现阶段可以理解为不同进程中的同名变量,其实id这个变量具有两个不同值跟进程地址空间有关,后续会进行深入学习

6.父子进程被创建好,fork()之后谁先运行呢?

谁先运行由调度器决定,不是我们能决定的,调度器的本质是在特定的数据结构中执行查找的一套算法,会在多个pcb中找到合适的进程放在cpu中去运行,一般每个进程的调度时间尽量相同来公平调度.cpu负责把一个进程放到cpu上去运行,调度器来决定哪一个进程被运行

3.2 进程状态

3.2.1 看看linux内核源代码是怎么说的

1 为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状态(在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 */
};

2 R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏ 队列⾥。
3 S睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
4 D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
5 T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。
6 X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。

3.2.2 进程状态查看

复制代码
ps aux / ps axj 命令

1 a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
2 x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
3 j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
4 u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等

3.2.3 Z(Zombie) - 僵尸进程

僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后⾯讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程
僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态
来⼀个创建维持30秒的僵死进程例⼦:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 1;
    }
    else if (id > 0)
    { // parent
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30);
    }
    else
    {
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    }
    return 0;
}

开始测试:

打开监控:

运行测试代码后的结果:

3.2.4 僵尸进程的危害

1 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
2 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
3 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进⾏开辟空间!
4 内存泄漏?是的!
5 如何避免?后⾯讲

3.2.5 孤儿进程

1 ⽗进程如果提前退出,那么⼦进程后退出,进⼊Z之后,那该如何处理呢?
2 ⽗进程先退出,⼦进程就称之为"孤⼉进程"
3 孤⼉进程被1号init/systemd进程领养,当然要有init/systemd进程回收喽。

测试代码:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 1;
    }
    else if (id == 0)
    { // child
        printf("I am child, pid : %d\n", getpid());
        sleep(10);
    }
    else
    { // parent
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);
        exit(0);
    }
    return 0;
}

结果:

3.3 进程优先级

3.3.1 基本概念

1 cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2 优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性能。
3 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤ 改善系统整体性能。

3.3.2 查看系统进程

在linux或者unix系统中,⽤ps ‒l命令则会类似输出以下⼏个内容:


我们很容易注意到其中的⼏个重要信息,有下:
UID : 代表执⾏者的⾝份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏
NI :代表这个进程的nice值

3.3.3 PRI 和 NI

1 PRI也还是⽐较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼
2 那NI呢?就是我们所要说的nice值了,其表⽰进程可被执⾏的优先级的修正数值
3 PRI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
4 这样,当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快被执⾏
5 所以,调整进程优先级,在Linux下,就是调整进程nice值
6 nice其取值范围是-20⾄19,⼀共40个级别。

3.3.4 PRI vs NI

1 需要强调⼀点的是,进程的nice值不是进程的优先级,他们不是⼀个概念,但是进程nice值会影响到进程的优先级变化。
2 可以理解nice值是进程优先级的修正数据

3.3.5 查看进程优先级的命令

⽤top命令更改已存在进程的nice:

top
进⼊top后按"r"‒>输⼊进程PID‒>输⼊nice值
注意:
其他调整优先级的命令:nice,renice
系统函数:

cpp 复制代码
#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

3.3.6 补充概念-竞争、独⽴、并⾏、并发

1 竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为了⾼效完成任务,更合理竞争相关资源,便具有了优先级
2 独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
3 并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
4 并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发

3.4 进程切换

CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运⾏, 这⼀过程就是context switch。

3.5 linux内核进程O(1)调度队列

3.5.1 一个cpu拥有一个runque

如果有多个CPU就要考虑进程个数的负载均衡问题

3.5.2 优先级

普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0〜99(不关⼼)

3.5.3 活动队列

(1)时间⽚还没有结束的所有进程都按照优先级放在该队列
(2)nr_active: 总共有多少个运⾏状态的进程
(3)queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以,数组下标就是优先级!
(4)从该结构中,选择⼀个最合适的进程,过程是怎么的呢?

  1. 从0下表开始遍历queue[140]
  2. 找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
  3. 拿到选中队列的第⼀个进程,开始运⾏,调度完成!
  4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
    (5)bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个⽐特位表⽰队列是否为空,这样,便可以⼤ 提⾼查找效率!

3.5.4 过期队列

1 过期队列和活动队列结构⼀模⼀样
2 过期队列和活动队列结构
3 过期队列上放置的进程,都是时间⽚耗尽的进程
4 当活动队列上的进程都被处理完毕之后,对过期队列的进程进⾏时间⽚重新计算

3.5.5 active指针和expired指针

1 active指针永远指向活动队列
2 expired指针永远指向过期队列
3 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间⽚到期时⼀直都存在的。
4 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批新的活动进程!

3.4.6 总结

在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法!

cpp 复制代码
struct rq
{
    spinlock_t lock;
    /*
     * nr_running and cpu_load should be in the same cacheline because
     * remote CPUs use both these fields when doing load calculation.
     */
    unsigned long nr_running;
    unsigned long raw_weighted_load;
#ifdef CONFIG_SMP
    unsigned long cpu_load[3];
#endif
    unsigned long long nr_switches;
    /*
     * This is part of a global counter where only the total sum
     * over all CPUs matters. A task can increase this counter on
     * one CPU and if it got migrated afterwards it may decrease
     * it on another CPU. Always updated under the runqueue lock:
     */
    unsigned long nr_uninterruptible;
    unsigned long expired_timestamp;
    unsigned long long timestamp_last_tick;
    struct task_struct *curr, *idle;
    struct mm_struct *prev_mm;
    struct prio_array *active, *expired, arrays[2];
    int best_expired_prio;
    atomic_t nr_iowait;
#ifdef CONFIG_SMP
    struct sched_domain *sd;
    /* For active balancing */
    int active_balance;
    int push_cpu;
    struct task_struct *migration_thread;
    struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
    /* latency stats */
    struct sched_info rq_sched_info;
    /* sys_sched_yield() stats */
    unsigned long yld_exp_empty;
    unsigned long yld_act_empty;
    unsigned long yld_both_empty;
    unsigned long yld_cnt;
    /* schedule() stats */
    unsigned long sched_switch;
    unsigned long sched_cnt;
    unsigned long sched_goidle;
    /* try_to_wake_up() stats */
    unsigned long ttwu_cnt;
    unsigned long ttwu_local;
#endif
    struct lock_class_key rq_lock_key;
};
/*
 * These are the runqueue data structures:
 */
struct prio_array
{
    unsigned int nr_active;
    DECLARE_BITMAP(bitmap, MAX_PRIO + 1); /* include 1 bit for delimiter */
    struct list_head queue[MAX_PRIO];
};
相关推荐
_OP_CHEN2 小时前
Linux网络编程:(八)GCC/G++ 编译器完全指南:从编译原理到实战优化,手把手教你玩转 C/C++ 编译
linux·运维·c++·编译和链接·gcc/g++·编译优化·静态链接与动态链接
阿乐艾官2 小时前
【十一、Linux管理网络安全】
linux·运维·web安全
weixin_537765803 小时前
【负载均衡】LVS DR模式详解
服务器·负载均衡·lvs
LoneEon3 小时前
告别手动操作:用 Ansible 统一管理你的 Ubuntu 服务器集群
运维·服务器·ansible
Code Warrior3 小时前
【Linux】Socket 编程预备知识
linux·网络·c++
摘星|4 小时前
架设一台NFS服务器,并按照以下要求配置
linux·运维·服务器
做运维的阿瑞4 小时前
Linux环境变量持久化完全指南
linux·运维·服务器
天才奇男子5 小时前
从零开始搭建Linux Web服务器
linux·服务器·前端
Mr_Dwj5 小时前
【运维】GNU/Linux 入门笔记
linux·运维·gnu