Linux---进程概念
1 冯诺依曼体系结构
1-1 理解冯诺依曼体系
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。


当代计算机是性价比产物,芯片技术 + 摩尔定律
1-2 理解数据流动
我们直接用qq聊天来举例

数据流动本质:拷贝
2 操作系统(operator system)
2-1 概念

2-2 设计OS的目的

注意:
1 软硬件体系结构层状结构
2 访问操作系统,必须使用系统调用 -- 其实就是函数只不过是系统提供的
3 我们的程序,只要你判断出它访问了硬件,那么它必贯穿整个体系结构!
4 库可能在底层做了封装
2-3 核心功能
2-4 如何理解"管理"
我们通过学生,辅导员,校长之间的关系来理解一下管理

我们思考一个问题? 校长管理学生要自己一个一个去管理吗?实际上是不用的,校长想要知道学生的出勤情况,以及了解这个学生的基本信息做出安排,只需要看辅导员专门收纳学生信息的数据表就行了,然后根据数据表做出安排,然后让辅导员去执行落实,校长和学生实际上从来没有见过面。那么在日常的校长在管理学生的工作就变成了对于中间层数据传上来形成的数据表的增删查改
结论:
1 要管理者,管理者和被管理者,可以不需要见面
2 管理者和被管理者,根据"数据"进行管理
3 不需要见面,如何得到数据? 由中间层得到
总结:
管理 就是先描述,再组织
描述起来,用struct结构体。组织起来,用链表或其他高效的数据结构
2-5 系统调用和库函数概念
理解系统调用和库:
就像银行不信任顾客取钱的时候不可能让顾客直接进金库里面拿,而是开启一个一个窗口让用户来窗口处取钱而这些窗口就是系统调用的接口函数
库就是一些开发者发现系统调用对用户要求比较高或者为了提高代码的可移植性所以对系统调用函数做了适度的封住,例如C语言库

系统调用和库的关系:
系统调用和库其实就是上下层的关系,就像银行系统和服务窗口一样,只是银行给用户提供服务的地方

3 进程
3-1 基本概念和基本操作
3-1-1 描述进程PCB
基本概念
task_struct-PCB 的一种

3-1-2 task_ struct

3-1-3 初步理解以及查看进程
1. 进程的信息可以通过 /proc 系统文件夹查看


ps ajx 查看所用进程,用 && 可以同时实行两份指令
注意:
从这里看,bash也是个进程,所以我们可以得到一个结论就是命令行解释器本质上就是一个进程,我们历史上执行所有指令,工具,自己程序,运行起来,全部都是进程
3. 进程中自带了 cwd 会记录下自己当前的工作路径 还有 exe 会记录自己进程所对应可执行程序文件

3-1-5 通过系统调用获取进程标示符
• 进程id(PID)
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 printf("hello world\n");
7 printf("pid:%d,ppid:%d\n",getpid(),getppid());
8 return 0;
9 }

3-1-6 通过系统调用创建进程-fork初识

1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 int ret =fork();
7 printf("hello process:%d,ret:%d\n",getpid(),ret);
8 return 0;
9 }

• fork 后通常用 if 进行分流,子进程的返回值是0,父进程的返回值是那个子进程的pid
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 int ret =fork();
8 if(ret==0)
9 {
10 printf("我是子进程,我的pid是:%d\n",getpid());
11 exit(1);
12 }
13
14 printf("我是父进程,我的pid是:%d\n",getpid());
15
16 return 0;
17 }

3-1-7 理解子进程--fork

1 fork会有两个不同的返回值而且fork函数会返回两次,并且父进程和子进程的数量比为 1 : n 。因为父子进程在fork函数内部就已经完成了子进程的创建和调用所以子进程也会执行fork的return语句
2 为什么在上述中一个变量,即 ==0 ,又大于0?导致 if else 同时成立?是因为子进程会对哪一个变量进行写时拷贝所以虽然是同一个变量名但是实际上在底层物理空间中区分开了

结论: 进程具有独立性
3-2 进程状态
3-2-1 初步理解进程状态
1 课本上的说法 -- 名词提炼

2 运行 && 阻塞 && 挂起
运行:进程在调度队列,进程的状态都是running

阻塞:等待某种设备或者资源就绪 (键盘,显示器,网卡,磁盘,摄像头,话筒 ....)

挂起:当内存不足时,在磁盘上有一个swap交换区就会将一些数据代码拷贝到swap上当这个过程就是挂起,就是为了缓解内存压力

3-2-2 理解内核链表的话题
我们从上面可以看到在内核中可以看到,struct device 为什么可以挂在在task_struct 上,因为他们不是像链表一样直接存放的是节点,而是在每一个结构体中存放指针,然后通过这个指针在整个结构体的偏移量就可以找到对应的指针进行链接

3-2-3 Linux内核源代码怎么说
/*
*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)":可中断休眠,浅睡眠,例如读取外设输入就是 S 状态(阻塞状态),信号可以唤醒
"D (disk sleep)":不可中断休眠,深度休眠,常用于磁盘文件读取时候的 D 状态(阻塞状态),信号不可以杀死
"T (stopped)":进程完全暂停执行,但是进程可以被再次执行
"t (tracing stop)":调试断点暂停,gdb 使用
"X (dead)":进程退出
"Z (zombie)":僵尸状态,为了让父进程可以获得推出信息
3-2-4 进程状态查看
ps aux / ps axj 命令

3-2-5 Z(zombie)-僵尸进程
• 我们创建子进程的目的就是为了让子进程完成某种事情,所以为了知道事情完成的程度和情况我们父进程就必须从子进程中得到放在task_struct的信息

3-2-6 僵尸进程危害
3-2-7 孤儿进程
扩展:
kill -9 进程pid
3-3 进程优先级
3-3-1 基本概念
优先级是什么?
是进程得到CPU资源的先后顺序,注意和权限做区分,权限是决定能否得到某种资源,而优先级是决定得到资源的先后顺序
为什么要有优先级?
目标资源短缺,导致要通过优先级决定谁先谁后
优先级是怎么实现的?
通过PRI:决定进程的优先级,NI:进程优先级的修改数据,这俩个数据来决定优先级,PRI+NI的值越低,优先级越高,反之越低,是基于时间片的分时操作系统,考虑公平性,优先级可能发生变化,但是变化幅度不大
优先级的意义
3-3-2 查看系统进程
3-3-3 PRI and NI
• PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

3-3-4 PRI vs NI
3-3-5 补充概念-竞争、独立、并行、并发

3-4 进程切换
3-4-1 理解死循环
死循环的运行:
a. 一旦一个进程占据CPU,会把自己的代码跑完吗? 不会!! 时间片的东西
b. 死循环进程,不会打死系统,不会一直占据CPU而是进行进程切换
3-4-2 理解CPU和寄存器
寄存器就是CPU内部的临时空间
寄存器 != 寄存器内部的数据,寄存器是空间内存,寄存器内部的数据是内容是多变的

3-4-3 理解进程切换


进程切换,最核心的,就是保存和恢复当前进程的硬件上下文数据,既CPU内寄存器的内容!!

结论:
所以当前进程要把自己的硬件的上下文数据,保存起来,保存在哪里?显而易见,保存在了task_struct的TSS(任务状态段)内部
3-5 Linux内核调度算法
3-5-1 认识调度队列

上图是Linux2.6内核中进程队列的数据结构
注意:⼀个CPU拥有⼀个runqueue,如果有多个CPU就要考虑进程个数的负载均衡问题
3-5-2 从运行队列理解优先级
• 普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
3-5-3 理解 active 指针和 expired 指针
3-5-4 理解O(1)调度算法
1 挑队列
队列分类
队列分为active 活动队列和expired过期队列
队列作用
active 活动队列:存放时间片(一个进程单次占用CPU的时间)还没有耗尽的就绪进程,调度器优先从这里选任务运行。
expired 过期队列:用来存放时间片已经用完的就绪进程。 进程刚挪进过期队列的时候,内核就已经给它分配好了新的时间片,只是这一轮调度周期(epoch)不会运行它。 等到活动队列里所有进程时间片全部耗光、活动队列为空时,直接交换两个队列的指针。过期队列就变成新的活动队列,里面的进程带着之前已经分配好的时间片,进入下一轮调度;交换队列这个动作本身,不会再重新计算时间片。
队列的内容

队列挑选
调度器优先使用活动队列,通过判断nr_active活动队列里还有没有就绪进程。进程在活动队列上运行,单个进程时间片用完,就把这个进程挪到过期队列,并分配新的时间片。
随着一个个进程耗尽时间片,活动队列里的进程不断变少。当活动队列的 nr_active 变为 0,也就是活动队列里没有可运行的普通进程时,交换两个队列的指针:原来的过期队列变成新的活动队列,原来已经空掉的活动队列变成新的过期队列,继续下一轮调度。

2 挑进程
4 命令行参数和环境变量
4-1 命令行参数
4-1-1 基本概念
执行程序的时候,在命令后面跟的参数,就是命令行参数。C 语言程序通过 main 函数两个参数接收
int main(int argc,char* argv[])
{
return 0;
}
4-1-2 理解命令行参数表
bash 作为命令行解释器,读取用户输入,解析并构造命令行参数表(字符串指针数组),数组末尾以NULL作为结束标记。无引号的空格会被 bash 当作分隔符切分成多个参数。被引号包裹的含空格内容整体作为一个参数,不会被切分。
./prog hello world #空格作为分割符
./prog "hello world" #空格被引号包裹,作为参数选项放入命令行参数表
bash 构造完参数表后,将它传递给新创建的应用程序,供程序 main 函数的 argv 使用,参数表归属于新程序,bash 不会长期维护该表,参数表中argv[0] 存放程序自身的名称,后面依次为各个命令行选项与参数。
例:
1 #include <stdio.h>
2
3 int main(int argc,char*argv[])
4 {
5 int i=0;
6 for(i=0;i<=argc;i++)
7 {
8 printf("argc[%d]:%s\n",i,argv[i]);
9 }
10 return 0;
11 }


4-2 环境变量
4-2-1 基本概念
( : 是Linux下的路径分隔符)
4-2-2 理解环境变量
• 从存储角度理解,环境变量会在 bash 形成一张表 --- 环境变量表
• 系统中存在环境变量,来帮助系统找到目标二进制文件

环境变量组织方式环境变量表

4-2-3 常见环境变量
• PATH : 指定命令的搜索路径
4-2-4 查看环境变量的方法
echo $NAME //NAME:你的环境变量名称

4-2-5 结合环境变量理解命令
从我们自己的可执行程序和系统的命令来看,为什么我们的可执行程序在运行的时候,都要加上 ./ 但是系统的命令例如 ls 就不需要加 ./

这是因为在环境变量中有一个PATH的环境变量,当我们在执行命令的时候,系统就会通过环境变量在系统中找到对应的可执行程序,但是我们自己的代码就需要点明路径因为没有相对应的环境变量
4-2-6 获取环境变量的方法
1 操作
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
export 键 = 值

3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
注意:export创建的环境变量会在服务器断链即bash进程结束的时候清除,如果想要永久保存那要在bash的环境变量配置表里新增
2 代码
• 命令行的第三个参数
1 #include <stdio.h>
2
3 int main(int argc,char*argv[],char* env[])
4 {
5 int i = 0;
6 for(; env[i]; i++){
7 printf("%s\n", env[i]);
8 }
9 return 0;
10 }
• 第三方全局变量environ

1 #include <stdio.h>
2 int main(int argc, char *argv[])
3 {
4 extern char **environ;
5 int i = 0;
6 for(; environ[i]; i++){
7 printf("%s\n", environ[i]);
8 }
9 return 0;
10 }
• 系统调用,getenv(),putenv()

4-2-7 理解环境变量的特性
• 环境变量具有全局属性,可以被子进程继承下去
• bash 会记录两套变量
环境变量: 通过 export这个内建命令(不需要创建子进程,而是让bash自己亲自执行,bash自己调用函数,或者系统调用完成),来创建的变量
export i
本地变量:直接设置得到,不会被子进程继承,只在bash内部使用
i=10
5 程序地址空间
5-1 程序地址空间回顾
我们回顾我们之前学习C/C++语言的时候,我们进程讲的内存空间结构图
5-2 引入虚拟地址
我们先来看一个现象
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <stdlib.h>
5
6 int main(int argc,char*argv[],char* env[])
7 {
8 int a=10;
9 int ret =fork();
10 if(ret == 0)
11 {
12 a++;
13 printf("变量a的值为:%d,它的地址是%p\n",a,&a);
14 exit(1);
15 }
16 int status=0;
17 waitpid(ret,&status,0);
18 printf("变量a的值为:%d,它的地址是%p\n",a,&a);
19 return 0;
20 }

明明子进程已经对变量a进行修改了,发生了写时拷贝,确实打印输出的变量a的值也确实不同,但是为什么我们取地址却对应的时一样的地址值呢?
这是因为这个地址不是实际我物理地址,在Linux下这个地址叫做虚拟地址。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址 。
5-3 进程地址空间
所以之前说 ‘ 程序的地址空间 ’ 是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?

这个就可以解释之前看到的现象,同一个变量,地址相同,其实是虚拟地址相同,但是内容可能被映射到不同的物理内存上。
举例理解
一个大富翁分家产,为了不寒几个孩子的心,都说会将所有家产都给他们,这就是我们常见的画饼,其实大富翁就是操作系统,而孩子就是经常,这个所谓的饼就是虚拟地址空间,虽然都说将所有的家产给你即让每个进程认为它一个人独占4GB的物理空间,实际上没有独占而是画的饼

问题来了,饼要不要管理? 当然是要的,也是先描述,再组织的管理方法,所以虚拟地址空间本质也是一个数据结构。
那是用什么方式记录呢?记录在一个结构体内部,而且它只记录每一个段的开始和结束的地址,对整个虚拟地址空间进行区域划分,来管理整个虚拟地址空间
总结进程创建的流程
1.在虚拟地址空间中申请指定大小的空间,调整区域划分
2.加载程序,申请物理空间
3. 进行页表映射,将物理地址转化为虚拟地址,提供给上层用户使用
5-4 虚拟内存管理
struct task_struct
{
/*...*/
struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他
//的虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。
struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当
//该进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因
//为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
/*...*/
}

struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}
struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;


5-5 为什么要有虚拟地址空间
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐




所有评论(0)