1.冯诺依曼体系
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系 。
组成部分
- 输入设备:键盘、话筒、摄像头、磁盘、网卡…
- 输出设备:显示器,音响,磁盘,网卡,显卡…
- 中央处理器(CPU):算数计算+逻辑运算
- 存储器:内存
为什么要有内存?
-
1.从技术的角度:
- CPU的运算速度 > 寄存器的速度 > L1~L3Cache【三级缓存】> 内存 > 外设【磁盘】> 光盘磁带
- 外设不会之间和CPU交互,而是先和内存交互。然后CPU再和内存进行交互
- 从整个内存来看,整个体系结构就是一个大的缓存,解决外设和CPU速度不匹配的问题
-
2.从成本角度
1.2操作系统
操作系统包括:
- 内核:内存管理,进程管理,文件管理,驱动管理
- 其他程序
操作系统可以看成是管理软件的软件。
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分
由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统
调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发
2.进程
2.1进程的概念
- 进程是一个运行起来的程序(程序是一个可执行的文件)
- 内核:担当分配系统资源(CPU时间,内存)的实体。
- 进程=可执行文件+描述进程的数据结构(task_struct)
为什么管理描述进程要有-PCB(process ctrl block)?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct 。(因为Linux内核是用C语言写的,所以Linux管理内核就使用一个结构体存储一个进程的信息,以便管理)
- 在Linux中,所有运行在系统里的进程都以task_struct链表的形式存在内核里
查看进程
查看进程
ls /proc
也可以使用psc
ps ajx
2.2 task_struct
task_ struct内容分类
- 标示符pid: 描述本进程的唯一标示符,用来区别其他进程。
- 状态state: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共有的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
为什么父进程的父进程不变?
所有进程的最终父进程是bash进程,几乎我们在命令行上所执行的所有的指令(cmd),都是bash进程的子进程。
为什么fork(),子进程会返回0,而父进程会返回子进程的pid?
子进程的task_struct对象,内部的数据基本上都是从父进程拷贝而来的。fork()之后,父子进程的代码共享,但是一般都通过不同的返回值,让不同的进程执行不同的代码。【而数据是各自独立】
如何理解进程被运行?
理解调度器和调度队列(runqueue)
2.3进程的状态
操作系统层面的进程的状态status
- 进程运行:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在调度【运行】队列里。
-
- 进程终止:表示这个进制永远不再使用,随时等待被释放
-
- 进程阻塞:
- 1.一个进程,在使用资源的时候,不仅仅要申请CPU资源
- 2.进程可能要申请更多的其他资源:磁盘、网卡、显卡、显示器资源…
- 3.在申请CPU资源的时候,可能暂时无法得到满足,需要排队-----运行队列
- 4.如果要申请其他的慢资源(比如外设)------也需要进行排队【申请各个资源的等待队列】(task_struct排队)。
- 5.当进程申请其他某些资源,而资源暂时没有准备好,或者在为其他进程提供服务。此时:1.当前进程要从runqueue中移除。2.将进程的task_struct放入对应设备的描述结构体中的等待队列。
- 6.当进程在等待某种资源的时候,资源没有就绪,进程需要在该资源的等待队列中进行排队,进程的代码不会被执行,也就是进程阻塞。
-
- 进程挂起:
- 当进程过多时,内存可能不足,这是OS就会对部分进程进行辗转腾挪
- 短期时间不会被调度的进程(可能是因为某资源的等待队列过长,短期内无法申请到某资源),它的代码和数据依旧在内存中占用资源,就是白白的浪费空间。OS就会把进程的代码和数据从内存置换到磁盘中【磁盘中有一个专门存放置换进程代码和数据的分区,叫做swap分区】。
- 因为内存不足和展示不被调度,而导致进程的数据置换到磁盘的swap分区,就叫做进程挂起。
系统为什么要维护一个终止态?
- 可能系统需要进程终止的信息
- 释放进程需要时间,可能此时操作系统比较繁忙。
- 该状态下,进程永远也不运行,随时等待被释放。
Linux的进程状态state
task_struct中使用task_state_array[]数组存放状态变量
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)),该进程可以被OS杀死。
- D磁盘休眠状态(Disk sleep)-----对应阻塞状态:有时候也叫不可中断睡眠状态(uninterruptible sleep),**此状态下OS无法杀死该进程,**在这个状态的进程通常会等待IO的结束。
- 一般而言,Linux中,如果我们等待的是磁盘资源,进程阻塞的状态就是D。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- t(tracing stop):也是停止状态,是调试状态下的暂停状态。
- X死亡状态(dead):这个状态只是一个返回状态,进程可以被释放,你不会在任务列表里看到这个状态。
- Z僵尸状态(zombie):在Linux中,一个进程退出的时候,一般不会直接进入X状态(死亡状态,资源立马回收),而是进入Z状态。
- 为什么?
- 一个进程被创建出来,一定是因为要有认为让这个进程执行,当该进程退出的时候,需要告知父进程和OS执行任务的情况。
- 进程进入Z状态,就是为了维护退出信息,可以让父进程或者OS读取
- 僵尸进程就是Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间 。
孤儿进程:父进程先死,子进程还没有被杀掉。子进程会被1号进程领养。而1号进程就是【操作系统】。
进程的大部分时间都在等待申请资源
int main()
{
printf("begin.....\n");
while(1)
{
}
return 0;
}
int main()
{
printf("begin.....\n");
while(1)
{
printf("hello world\n");
}
return 0;
}
ps ajx | head -1 && ps ajx | grep hello
表明进程大部分的时间都在申请资源。
2.4进程优先级
优先级VS权限
优先级是进程获取资源的先后顺序。
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
为何会存在优先级
资源不够。系统里面永远都是,进程占大多数,而资源是少数。
Linux下的优先级相关概念
ps -la
相关概念
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI和NI
- PRI,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- NI,就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值;nice其取值范围是-20至19,一共40个级别。Linux给用户用的进程优先级范围是80-100。
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
**用top命令更改已存在进程的nice **
- top
- 进入top后按“r”–>输入进程PID–>输入nice值
- 或者PID to renice
- Linux不允许用户无节制的设置优先级
2.5其他重要概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发 。
单道和多道程序设计
单道程序设计:一次只有一个处于程序在运行中;其他程序处于等待状态。(DOS)
多道程序设计:设计出时间片,在一个时间片中执行一个程序,在下一个时间片立马然后切换为下一个程序,从而让出cpu的资源给其他程序。
由于一个时间片的时间很短,属于是毫秒级别的。所以在人的感知上,几个程序是并发进行的;但是在微观上,在一个时间片上,只有一个程序在运行。微观上串行,宏观上并行
进程的切换是依靠调度器。
- 在一段时间内,多个进程都会通过切换交叉的方式,让多个进程在这段时间内得到推进,这种现象叫做并发。
操作系统,就是简单的根据队列进行先后调度吗?有没有可能突然来了一个优先级更高的进程?
- 当代计算机,都符号抢占式内核。
- 正在运行的低优先级进程,如果来了优先级更高的进程,调度器会直接把进程从CPU上剥离(无论低优先级的时间片是否跑完),给优先级高的进程,这就是抢占式内核。
2.5Linux维护进程的方式(O(1)调度)
依靠hash表和位图
task_struct* queue[140];
根据不同的优先级,将特点的进程放入相应的队列中。
比如优先级为2的进程,放入第二个task_struct queue中。
维护方式:
当新增一个进程时(进程的优先级为n),将改进程放入queue[n]队列的末尾。这种方式也维护了进程的优先级,即高优先级的先运行。
进程切换
CPU内的寄存器,可以临时的存储数据
int hello()
{
int a=10+20;
return a;
}
int b=hello();
/*
当返回值较小时
a是临时变量,在返回时被销毁,系统会把a对应的值放入寄存器eax中;
然后寄存器eax再将值mov给b对应的内存
*/
进程进行切换的时候,进程需要被CPU剥离。
- 上下文数据保存在哪?
- Linux的PCB,task_struct中有一个保存上下文数据的字段。
3.环境变量
3.1常见环境变量
PATH : 指定命令的搜索路径 [重点]
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)[重点]
SHELL : 当前Shell,它的值通常是/bin/bash
查看环境变量
env $NAME //NAME:你的环境变量名称
1. echo: 显示某个环境变量值[重点]
2. export: 设置一个新的环境变量[重点]
用法:export 本地变量名
3. env: 显示所有环境变量[重点]
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
3.2环境变量PATH
可执行程序的搜索路径是保存在一个全局变量中,PATH,给系统提高命令的搜索路径。是环境变量中的一个
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
#每个目录中间使用冒号隔开。
#PATH的搜索顺序:从左向右依次进行程序的搜索,找到就执行程序,找不到就找下一个目录。如果全部都没有,就报错:command not found!
#也就是执行路径下的可执行文件可以直接使用(可以不指出相应的路径)
向PATH中添加路径
#方法一:
$ PATH="${PATH}:/root"
#在该用户下的环境变量中添加路径/root
#方法二:
$ export PATH=$PATH:/root
#在该用户下的PATH环境变量中添加路径/root
命令行变量
3.3环境变量的C/C++获取方式
main函数的参数
int main(int argc,char* argv[])
{
/*
argc:参数个数
argv:指针数组,并且第一个字符串指向的是这个可执行程序的名字,最后一个字符串指向的是NULL
*/
}
- 给main函数传递的argc,char* argv[],传递的是命令行参数的个数和选项。
- argv[]中第一个指向的是程序名,最后一个指向的是NULL。
-
main函数还可以传递第三个参数char env[ ]----->环境变量*
**实现命令行计算器 **
#include<string.h>
#include<stdio.h>
#include<unistd.h>
int main(int agrc,char* argv[])
{
if(argc!=4)
{
printf("Useage:[-a][-s][-m][-d]\n");
}
int x=atoi(argv[1]);
int y=atoi(argv[2]);
if(strcmp("-a",argv[3])==0)
{
printf("%d-%d=%d\n",x,y,x-y);
}
else if(strcmp("-s",argv[3])==0)
{
printf("%d+%d=%d\n",x,y,x+y);
}
else if(strcmp("-m",argv[3])==0)
{
printf("%d*%d=%d\n",x,y,x*y);
}
else if(strcmp("-d",argv[3])==0)
{
printf("%d/%d=%d\n",x,y,x/y);
}
return 0;
}
获取环境变量
遍历环境变量
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
#include<stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
实现一个只能被本用户调用的程序
int main()
{
char * var=getenv("USER");
if(strcasecmp(var,"west")!=0)
{
printf("没有权限...........\n");
}
else
{
printf("执行成功.........\n");
}
return 0;
}
一个函数声明的时候没有带参数,可以传参吗?
int fun()
{
printf("hello world\n");
}
int main()
{
fun(10,20,30,40);
return 0;
}
答案是可以传递参数。但是不能写成 fun(void),这就表明函数一定不能传递参数。
3.4本地变量和环境变量
- 环境变量的全局性:环境变量会被子进程继承下去。
- 本地变量:本质是在bash内部定义的变量,不会被子进程继承下去
- **内建命令:**Linux下大部分命令都是通过创建子进程执行,但是有一部分特殊的命令,是在bash执行(调用bash内部的函数实现特点的功能)
4.进程地址空间
每一个进程启动,操作系统都会给进程创建一个地址空间,每一个进程都有自己的进程地址空间。
操作系统为了管理这些进程地址空间,有数据结构mm_struct进行管理【管理该进程的进程地址空间】。
4.1进程的基本概念
进程的独立性:体现在进程相关的数据结构是独立的,代码和数据是独立的。
4.2MMU(内存管理单元)的作用
**在32位机器下,一个进程可以管理的虚拟内存空间大小为4G。**而实际上的物理空间大小不是4G。而物理内存和虚拟内存自己的对应和管理就是通过MMU和地址转换记录表进行。
比如在.data中存放了a=10,那么就可以通过MMU(记录了虚拟内存和物理内存的映射关系),将他的虚拟地址转换为物理地址,从而访问内容10。
内存访问级别:
-
0是最高的级别,内核访问的权限
-
3是允许用户访问权限级别的权限
映射问题:
4.3页表
页表------将进程地址空间和物理地址映射【左边是虚拟地址,右边是物理地址】
- 页表:也叫转换表(Translation Table).它负责MMU虚拟地址与物理地址之间的映射关系,由用户编写并存放在内存中。这样就可以将MMU与页表联系起来,进行转换工作了。页表由很多个
页表项
组成。
相对地址/逻辑地址/进程地址空间 --------> 映射物理地址
4.4读时共享写时拷贝原则
int val=100;
int main()
{
pid_t pid=fork();
if(pid==0)
{
int i=5;
while(i)
{
printf("I am father pro,pid=%d,val=%d",getpid(),val);
sleep(1);
i--;
}
val=200;
printf("begin chage................\n");
while(1)
{
printf("I am child pro=%d,val=%d\n",getpid(),val);
sleep(1);
}
}
else
{
while(1)
{
printf("I am father pro,pid=%d,val=%d\n",getpid(),val);
sleep(1);
}
}
return 0;
}
为什么进程地址一样,但是内容确不一样。
写时拷贝原则:
实现了进程的独立性
为什么fork()有两个返回值?
pid_t pid是属于父进程空间定义的变量,return返回的时候,pid发生了写时拷贝,所以有两个返回值。
4.5为什么有进程地址空间
- 指针越界的问题,保护内存安全
- 进行内存管理,提高运行效率。
- 对功能模块进行解耦