一、背景
冯.诺依曼体系结构
输入设备 | 键盘、鼠标、摄像头、话筒、磁盘、网卡... |
输出设备 | 显示器、声卡、磁盘、网卡... |
CPU | 运算器、控制器 |
存储器 | 一般就是内存 |
数据在计算机的体系结构进行流动,流动过程中,进行数据的加工处理,从一个设备到另一个设备,本质:这是一种拷贝!
数据设备间拷贝的效率,决定了计算机整机的基本效率。
在硬件数据流动角度,在数据层面
1.CPU不和外设打交道,CPU和内存打交道。
2.外设(输入和输出)的数据,要先放到内存中
eg。程序运行,为什么要加载到内存?
二、操作系统
1.OS概念:
操作系统是一个软硬件资源管理的软件。
广义上的认识:操作系统的内核 + 操作系统的外壳周边程序
狭义上的认识:只考虑操作系统的内核
2.结构示意图(简单示意图)
层状划分
3.尝试理解操作系统 --- "管理"
不需要管理者和被管理者直接接触
拿到数据才是目的,本质是对数据进行管理
任何管理
先描述,在组织
一个完整的操作系统包含对相关内容的管理和系统调用接口
为什么需要操作系统?
操作系统对下(手段)进行软硬件管理工作,对上层提供良好(高效、稳定、安全)的运行环境(目的)
三、进程
1.进程的理解
进程=内核task_struct结构体 + 程序代码和数据 (形象理解)
PCB(process control block):进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
linux下具体称呼为 struct task_struct { //linux进程控制块}
如何理解进程动态运行?
只要我们的进程task_struct将来在不同的队列中,进程就可以访问不同的资源。
2.进程task_struct本身内部的属性有哪些
1.启动
1> ./XXXX ,本质时让系统创建进程并且运行 -- 我们自己写的代码形成的可执行 == 系统命令 == 可执行文件。在linux中运行的大部分执行操作,本质上都是运行进程!
ps axj | head -1 && ps axj | grep 进程 :可用于查看当前进程状态并且加上标头
2> 每一个进程 都有自己的唯一标识符,叫做进程pid (unsignen int pid)
3> 一个进程想知道自己的pid? getpid函数
4> ctrl+c在用户层面上终止程序,kill -9 pid,可以直接杀掉进程标识符为pid的进程
2.进程创建的代码方式
1.pid 和 ppid(父进程与子进程)
pid_t getppid(void) --- 获得当前进程父进程的pid
fork函数,fork之后,父子代码共享
创建一个进程,本质上是系统多一个进程,多一个进程就是多个 1.内核task_struct 2.自己的代码和数据
提问,父进程的代码和数据是从磁盘加载而来的,那子进程的代码和数据是从何而来的?
默认是继承父进程的代码和数据。
2.而我们为什么要创建子进程?
答案是我们想让子进程跑跟父进程不一样的代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.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;
}
id = 0 => 子进程执行
id = 子进程pid => 父进程执行
杀掉父子进程任一进程都不会影响另一个进程的执行。不过另一个进程会跑到后台,这个后面再说
四、进程状态
1.linux的进程状态
struct tesk_struct
{//内部的一个属性int status;
};
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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
2.僵尸进程和孤儿进程
1.僵尸进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{ pid_t id = fork(); if(id == 0) { int cnt = 5; while(cnt--) { printf("i am child process! cnt: %d pid: %d\n", cnt, getpid());sleep(1); } } else { while(1) { printf("i am parent process! pid: %d\n", getpid()); sleep(1); } } return 0;
}
运行示意图
Z --- 表示僵尸状态
在子进程执行完后,就变成了一个
僵尸进程:已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct 会记录自己的退出信息,未来让父进程来进行读取。
如果没有父进程读取,僵尸进程会一直存在!会引起内存泄漏问题
2.孤儿进程
修改一下代码,把cnt挪到父进程去,则
父进程如果先退出,子进程就会变成孤儿进程。孤儿进程一般都是会被1号进程(可看作OS本身)进行领养的。
孤儿进程为什么要被OS领养?因为依旧要保证子进程正常被回收。
我们直接在命令行启动的进程,它的父进程是bash,bash会自动回收新进程的Z
3.进程的阻塞、挂起和运行
让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,就叫做并发
任何时刻,都同时有多个进程在同时运行,就叫做并行
阻塞态:
挂起态:
进程切换:
CPU内部的所有的寄存器中的临时数据,叫做进程的上下文。
进程在切换,最重要的一件事情就是:上下文数据的保护和恢复
CPU内的寄存器:
寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套
CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据
寄存器!=寄存器的内容
五、进程优先级
1.什么是优先级
指定进程获取某种资源的先后顺序
task_struct 进程控制块 -> struct -> 内部字段 -> int prio = ???
linux中优先级数字越小,优先级越高
优先级:已经能,但是看顺序。 权限:能不能?的问题
2.为什么需要优先级
进程需要访问的资源(CPU)始终有限的,系统进程大都挺多的。
操作系统关于调度和优先级的原则:分时操作系统,基本的公平。如果进程因为长时间不被调度,就会造成饥饿问题
3.Linux优先级的特点和查看方式
PRI:进程优先级
NI:进程优先级的修正数据,nice值, 新的优先级 = 优先级 + nice ,达到对于进程优先级动态修改的过程。
注:1.nice值不能随意修改,它是有调整范围的。[-20,19] 40个数字,每次调整优先级,都是从80开始的。
修改方式:top->进入后按‘r’->输入pid->输入nice值
六、命令行参数和环境变量
1.命令行参数
int main(int argc, char* argv[])
这些参数可带可不带。
这些参数的意义:int argc, char* argv[]
先看现象
引出功能
为什么需要命令行参数?
本质,命令行参数本质是要交给我们程序不同的选型,用来定制不同的程序功能,命令中会携带很多的选项。
2.环境变量
为什么ls这种函数不需要带地址呢,而我们的process需要带上./?
linux中存在一些全局的设置,表明,告诉命令行解释器,应该去那些路径下寻找可执行程序
系统中很多配置,在我们登录linux系统的时候,已经被加载到了bash进程中(内存)。
如何添加环境变量?
最开始的环境变量不是在内存中,而是在系统的对应的配置文件中。
查看所有的环境变量: env
自己导入环境变量: export name=value
取消环境变量: unset name
本地变量
3.整体理解环境变量、系统和我们的程序结合
父进程的数据,默认能被子进程看到并访问。
bash进程启动时,默认会给子进程形成两张表
argv[]命令行参数表,environ[]环境变量表(从OS的配置文件来),bash通过各种方式交给子进程
环境变量具有系统级的全局属性,因为环境变量本身会被子进程继承下去
获取环境变量(代码级)
1.extern char **environ
2.通过main函数参数
3.getenv("PATH")
本地变量只在本bash内部有效,无法被子进程继承下去
导成环境变量。此时才能够被获取
echo export 为内建命令
七、程序地址空间(进程的地址空间)
1.引入
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> int g_val = 100; int main()
{ printf("fasther is running, pid = %d, ppid = %d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); pid_t id = fork(); while(1) { if(id == 0) { int cnt = 0; while(1) { printf("i am a child process, pid = %d, ppid = %d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); sleep(1); ++cnt; if(cnt == 5) { g_val = 300; printf("i am child process, g_val change %d -> %d\n",100,300); } } } else { while(1) { printf("i am a parent process, pid = %d, ppid = %d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val); sleep(1); } } }
}
父子进程具有独立性
注:这个地址绝不是物理地址,是虚拟地址
2.理解
子进程会把父进程的很多内核数据结构全拷贝一份
OS自主完成写时拷贝 --- 按需申请
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
通过调整拷贝的时间书匈奴,达到有效节省空间的目的
1.如何理解地址空间?
1>什么是划分区域?
地址空间结构体源代码
地址空间本质是内核的一个struct结构体!内部很多属性都是表示start end的范围
2.为什么要有地址空间?
1.将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域!
2.进程管理模块和内存管理模块进行解耦。
3.拦截非法请求 -> 对物理内存进行保护
3.如何进一步理解页表和写时拷贝
OS识别错误时:1.是不是数据不在物理内存 -> 缺页中断
2.是不是数据需要写时拷贝 -> 发生写时拷贝
3.如果都不是,才会进行异常处理。
4.如何理解虚拟地址
虚拟地址空间 -> 页表 -> 物理地址
提问:在最开始的时候,地址空间页表里面的数据从何而来?
程序里面本身就有地址 --- 虚拟地址(逻辑地址)
int main()
{ pid_t id = fork(); if(id == 0) { while(1) { printf("i am a child process id:%d, &id:%p\n",id,&id); sleep(1); } } else if(id > 0) { while(1) { printf("i am a parent process id:%d, &id:%p\n",id,&id); sleep(1); } } return 0;
}
fork() 之后需要return,return 的本质是对id进行写入,发生写时拷贝,虚拟地址相同,但物理地址不同
八、实例:linux2.6内核进程调度队列
进程调度大O(1)算法
两个array数组结构体,一个结构体只出不进,用actice指向。一个结构体只进不出,用expired指向。bitmap是位图,只需5次便可扫描出queue中是否还有数据。queue则管理各种优先级的进程,[100,139].arrive指向的结构体清空后,则交换active和expired两个指针。从而交替进行。