目录
冯诺依曼体系结构
截至目前,我们所认识的计算机,都是由一个个的硬件组件组成
- 输入设备:键盘,鼠标,话筒,网卡,磁盘…
- 输出设备:显示器,磁盘,打印机,网卡…
- CPU=运算器+控制器
- 存储器:内存
关于冯诺依曼体系,必须强调几点:
- CPU的获取、写入只能从内存中进行!
- CPU在数据层面,只和内存打交道,外设(输入设备和输出设备)只和内存打交道
操作系统
概念
操作系统是一款软硬件管理的软件
内核:进程管理、内存管理、文件管理
其他程序:例如函数库、shell程序…
设计OS的目的
- 对上,与硬件交互,管理所有的软硬件资源
- 对下,为用户提供一个良好的执行环境
- 软硬件体系结构层状结构
- 访问操作系统,必须使用系统调用
- 我们的程序只要判断出它访问了硬件,那么它必须贯穿整个软硬件体系结构
- 库可能在底层封装了系统调用
计算机管理硬件
描述
起来,用struct结构体
组织
起来,用链表
或者其他数据结构
系统调用和库函数
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用
- 系统调用在使用上,功能比较基础,对用户的要求相对较高,所以,有开发者对部分系统调用进行适度封装,形成了库。
进程
基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体
描述进程-PCB
- 基本概念
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
linux
操作系统下的PCB
是:task_struct
task_struct
- 内容
- 标识符:描述本进程的唯一标识符,用来区别其他进程
- 状态:任务状态,退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- …
- 组织进程
- 所有运行在系统里的进程都以
task_struct链表
的形式在内核里
- 所有运行在系统里的进程都以
查看进程
- 进程信息可以通过
/proc
系统文件夹查看
- 大多数进程信息也可以使用
top
和ps
来获取
#include<stdio.h>
int main()
{
while(1)
{
sleep(1);
}
}
$ ps ajx | grep code #查看指定进程code
$ ps aux | grep code #查看指定进程code
a:显示一个终端所有的进程,包括其他用户的进程
x:显示没有控制终端的进程,例如后台运行的守护进程
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
- 杀掉进程
ctrl+c
kill -9 pid
(对应进程的pid)
获取进程标识符
- 进程id (
PID
) - 父进程id (
PPID
)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid:%d\n",getpid());
printf("ppid:%d\n",getppid());
return 0;
}
通过系统调用创建进程
fork初识
在Linux中fork函数
是非常重要的函数,它从已经存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝到子进程
- 添加子进程到系统进程列表中
fork
返回,开始调度器调度
//code.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("父进程开始执行:%d\n",getpid());
int ret=fork();
if(ret<0)
{
perror("fork fail");
exit(1);
}
else if(ret==0)//child
{
printf("i am child:%d ,ret:%d\n",getpid(),ret);
}
else
{
//father
printf("i am father:%d ,ret:%d\n",getpid(),ret);
}
sleep(1);
return 0;
}
$ ./code
父进程开始执行:919385
i am father:919385 ,ret:919386
i am child:919386 ,ret:0
从上面的执行结果可以看出fork有2个返回值
fork之前
父进程独立执行,fork之后
,父子两个执行流分别执行。注意,fork之后
,谁先执行完全由调度器决定
fork函数返回值
- 子进程返回0
- 父进程返回的是子进程的pid
为什么一个函数会返回两次???
一个函数已经到return
了,核心工作就做完了
为什么fork给父子返回不同的返回值???
父:子=1:n
父进程拿到子进程的pid
方便对子进程进行管理
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int gval=100;
int main()
{
printf("父进程开始运行:%d\n",getpid());
pid_t id=fork();
if(id<0)
{
perror("fork fail");
return 1;
}
else if(id==0)
{
printf("我是一个子进程,我的pid:%d,我的父进程pid:%d\n",getpid(),getppid());
sleep(5);
//child
while(1)
{
sleep(1);
//printf("我是一个子进程,我的pid:%d,我的父进程pid:%d\n",getpid(),getppid());
printf("子进程修改变量:%d->%d\n",gval,gval+10);
gval+=10;//修改
printf("我是一个子进程,我的pid:%d,我的父进程id:%d,gval:%d\n",getpid(),getppid(),gval);
}
}
else
{
//father
while(1)
{
sleep(1);
printf("我是一个父进程,我的pid:%d,我的父进程pid:%d,gval:%d\n",getpid(),getppid(),gval);
}
}
}
写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
有写时拷贝技术的存在,所以父子进程得以彻底分离,完成了进程的独立性的技术保障
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
进程状态
本质:把PCB链入不同的队列中
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */ //可中断休息
"D (disk sleep)", /*2 */ //不可中断休息
"T (stopped)", /*4 */ //由ctrl+z引起
"t (tracing stop)", /*8 */ //由gdb引起
"X (dead)", /*16 */
"Z (zombie)", /*32 */
}
R运行状态
:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里S睡眠状态
:意味着进程在等待事件完成T停止状态
:可以通过发送SIGSTOP信号
给进程来停止进程。这个被暂停的进程可以通过发送SIGCONT信号
让进程继续运行X死亡状态
:这个状态只是一个返回状态,我们不会在任务列表里看到这个状态D磁盘休眠状态
:也叫不可中断睡眠状态,在这个状态的进程通常会等待IO
的结束
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Z-僵尸进程
- 僵尸进程是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回码时,就会产生僵尸进程
- 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
- 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
//模拟僵尸进程
int main()
{
int ret=fork();
if(ret<0)
{
perror("fork fail");
exit(1);
}
else if(ret==0)
{
printf("i am child:%d\n",getpid());
sleep(5);
}
else
{
printf("i am father:%d\n",getpid());
sleep(30);
}
}
- 僵尸进程的危害
Z状态
一直不退出,PCB
一直都要维护- 父进程如果一直不读取,那么子进程就一直处于
Z状态
- 一个父进程创建了很多子进程,就是不回收,会造成内存资源的浪费,内存泄漏
孤儿进程
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号进程领养,当然就由1号进程回收
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
//模拟孤儿进程
int main()
{
int ret=fork();
if(ret<0)
{
perror("fork fail");
exit(1);
}
else if(ret==0)
{
printf("i am child:%d\n",getpid());
sleep(30);
}
else
{
printf("i am father:%d\n",getpid());
sleep(5);
}
}
子进程的ppid
变了,父子进程中,如果父进程退出,子进程就要被1号进程领养。ctrl+c 杀不掉,必须使用kill -9 pid
为什么要领养??如果不领养,子进程进入僵尸后呢??
内存泄漏
进程优先级
- 是什么??
- 是进程得到
CPU
资源的先后顺序
- 是进程得到
- 为什么??
- 目标资源稀缺,导致要通过优先级确认谁先谁后
查看系统进程
在linux系统
中,用ps -l命令
则会类似输出以下几个内容:
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 922810 922809 0 80 0 - 2752 do_wai pts/2 00:00:00 bash
0 R 1000 927300 922810 0 80 0 - 2852 - pts/2 00:00:00 ps
UID
:代表执行者的身份PID
:代表这个进程的代号PPID
:代表这个进程的父进程PRI
:代表这个进程可被执行的优先级,其值越小越早被执行
NI
:代表这个进程的nice
值
PRI和NI
PRI
就是程序被CPU
执行的先后顺序,此值越小进程的的优先级越高NI
也就是nice
值,其表示进程可被执行的优先级的修正数值PRI(new)=PRI(old)+nice
- 调整进程优先级,在
linux
下,就是调整进程nice值
nice
其取值范围是-20~19
,一共40个级别
查看进程优先级的命令
sudo renice <优先级> -p <进程ID> #renice命令用于改变正在运行的进程的优先级
进程切换
- 死循环进程如何运行??
- 一旦一个进程占有
CPU
,不会把自己的代码跑完。因为有时间片 - 死循环进程不会打死系统,不会一直占有
CPU
- 一旦一个进程占有
时间片:当代计算机都是分时操作系统,每个进程都有它合适的时间片。时间片到达,进程就被操作系统从
CPU
中剥离下来
CPU
上下文切换:是任务切换/CPU寄存器切换
。当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是CPU寄存器
中的全部内容。
进程切换,最核心的就是保存和恢复当前进程的硬件上下文的数据,即CPU
内寄存器的内容
一个CPU一个运行队列
优先级
- 普通优先级:100~139
- 实时优先级:0~99(不关心)
活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列中
nr_active
:总共有多少个运行状态的进程queue[140]
:一个元素就是一个进程队列,相同优先级的进程按照FIFO
规则进行排队调度,所以,数组下标就是优先级!- 从该结构中,选择一个最合适的进程,过程是怎么的呢??
- 从0下标开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成
bitmap[5]
:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
过期队列
- 过期队列和活动队列结构一摸一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕后,对过期队列的进程进行时间片重新计算
active指针和expired指针
active指针
永远指向活动队列expired指针
永远指向过期队列- 活动队列上的进程越来越少,过期队列上的进程越来越多
- 在合适的时候,只要能够交换
active指针和expired指针
的内容,就相当于具有了一批新的活动进程