线程和进程
1. 进程
1.1 什么是进程
- 运行中的程序
1.2为什么要有进程呢?
-
为了任务能够交替执行。所以才有了进程的概念。
-
进程是怎样让任务交替执行的?
- 一个程序的用完一个时间片 进程会把程序的执行现场环境(各个cpu寄存器的值 以及PC)保存在进程的PCB中 。下次就可以接着上次的地方继续执行 所以任务就可以交替执行。
-
进程是什么 执行序列和资源+现场环境
1.3 进程是怎样被交替执行
1.4 进程的构成
1.5 进程之间之怎样被切换的
启动磁盘读写
// 设置当前进程状态为阻塞态
pCur.state='W'
将pCur放到DiskWaitQueue
schedule()
// 一 :schedule 函数
schedule(){
// 从就绪队列中选出下一个准备运行的进程
pNew=getNext(ReadyQueud);
// 切换到下一个进程
switch_to(pCur,pNew)
}
// 二:switch_to函数
switch_to(pCur,pNew){
// 保存上一个进程的执行进度
pCur.ax = CPU.ax;
pCur.bx = CPU.bx;
...
// 代码段基地址
pCur.cs = CPU.cs;
// pc: 指向下一条待执行的指令
pCur.retpc = CPU.pc;
// 恢复待执行进程的执行进度
CPU.ax = pNew.ax;
CPU.bx = pNew.bx;
...
CPU.cs = pNew.cs;
CPU.retpc = pNew.pc;
}
2. 线程
2.1 为什么要有线程呢?
- 进程切换效率低。怎么保证并发的优势 又能高效的切换 所以有了线程。
- 现场不切换资源(内存) 只切换指令序列
2.2 用户线程是怎么切换的呢?
- 核心是栈的切换
void Yield(){
TCB1.esp=esp;
esp=TCB2.esp;
}
2.3 用户线程的优缺点?
- 处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。所以还无法利用对核的优势 并且只要有一个线程堵塞 OS会认为整个进程都会被堵塞。
- 使用资源:与所属进程共享进程地址空间和系统资源。
- 调度:由在用户空间实现的线程库,在所属进程内进行调度
2.4 内核线程
2.4.1 为什么要有内核线程
- 为了能够利用多核优势,所以才有了内核线程
2.4.2 用户线程和核心线程是什么关系呢?常用的是一对一
2.4.3 内核线程的特点
-
内核线程创建和撤销都是操作系统干的事 所以操作系统可感知到线程的存在 ,可以利用多核的优势
-
内核态线程,创建成本高 通常我们会在内核中预先创建一些线程,并反复利用这些线程
2.5 内核线程是怎么实现的呢?(后续待完善)
- 程序
main()
{
A();
B();
}
A();
{
//fork是系统调用,引会引起中断
// fork 不仅可以引起切换 也可以引起切换
fork();
}
- _system_call : 初始化时将各种中断处理设置好
_system_call:
// 各种压栈 注意是压的是用户寄存器的值
push %ds..%fs
pushl %edx...
call sys_fork
pushl %eax
movl _current,%eax
// 判断当前线程是否阻塞
cmpl $0,state(%eax)
// 切换
jne reschedule
// 判断当前线程是否时间片用完
cmpl $0,counter(%eax)
// 切换
je reschedule
ret_from_sys_call:
// 进程切换函数
void schedule(void){
// 根据调度算法选择一个合适的线程
next=i;
// 切换到下个进程
switch_to(next); }
// 系统调用返回
ret_from_sys_call:
popl %eax // 回值popl %ebx ...
pop %fs ...
iret// 返回到int 0x80后面执行 mov res,%eax res=?
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,long ebx,long ecx,long edx, long
fs,long es,long ds,long eip,long cs,long eflags,long esp,long ss)
copy_process:
// 申请TCB
p=(struct task_struct *)get_free_page();
//创建内核栈
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
// 创建用户栈(和父进程共用的用户栈)
p->tss.ss = ss & 0xffff;
p->tss.esp = esp;
3. 进程调度(CPU调度算法)
3.1 什么是cpu调度算法
- 在就绪队列中选择哪个线程执行
3.2 一款很合理的调度算法 应该考虑什么?
- 短时间内让cpu执行更多的任务
3.3 基本的调度算法
- 先来先服务
- 特点:这似乎很公平 但是当一个长作业先运行了 ,那么后面的短作业等待的时间就会很长 不利短作业
- 短作业优先
- 特点:会有优先选择运行时间最短的进程来运行 这有助提高系统吞吐量 但是对长作业不利 很容易造成一种极端的现象 比如一个长作业在就绪队列等待运行 假如这个就绪队列有很多短作业 导致长作业不短往后排 导致长作业一直不会被运行
- 时间片轮转
- 特点 每个进程分配一个时间片 轮流执行 如果进程在时间片结束前提前结束活着阻塞 ,切换到其他进程上。但是用户更希望调度有优先级 每次调度优先级高的
- 高优先级优先
- 特点:每次选择优先级高的运行 如果优先级不能动态调整 那么将导致优先级低的一直被推后
3.4 Linux 0.11 的调度算法
-
时间片+动态调整优先级算法
-
作用counter:优先级 和 时间片
void Schedule(void)
//在kernel/sched.c中
{ while(1)
{
c=-1; next=0;
i=NR_TASKS;
// NR_TASKS 队列的length
p=&task[NR_TASKS];
while(--i){
// 1.从就绪队列中找出最大的counter运行
if((*p->state == TASK_RUNNING && (*p)->counter>c)
c=(*p)->counter, next=i;
}
if(c) break; //找到了最大的counter
// 动态调整优先级所有任务的的优先级 优先级=1/2*counter + priority(时间片)
// 如果任务被调度过那么 counter=0 即 优先级=1/2*0 + priority(时间片) 所有动态调整后 没运行的优先级会被调高
for(p=&LAST_TASK;p>&FIRST_TASK;--p)
(*p)->counter=((*p)->counter>>1)
+(*p)->priority; }
// 切换到下一个进程运行
switch_to(next);}
4. 进程的同步
4.1 什么是进程的同步
- 多个进程间走走停停 按照一定的顺序向前推进
4.2 怎么保证进程间走走停停—信号量
- 信号量:记录一些信息的量 并根据这信息来决定这个进程是唤醒还是睡眠
4.3 信号量的实现
-
struct semaphore { int value;// 记录资源的个数 PCB *queue; //记录等待在该信号量上的进程 } p(semaphore s) // 消费资源 v(semaphore s) // 生产资源 P(semaphore s){ s.value--; if(s.value<0){ sleep(s.quenue); } } V(semaphore s){ s.value++; if(s.value<=0){ wakeup(s.quenue); } }
4.4 信号量的解决生产者和消费者的问题
5. 信号量临界区保护
5.1 为什么要保护信号量
- 假如多个生产着 ,对信号量i进行修改 如果不保护可能导致最终信号量的值是错误的,所以就要互斥进入,如果一个进程在临界区中执行 则其他进程不允许进入
5.2 好的临界区保护原则
-
互斥进入
-
有空让进
-
有限等待
5.3 两个进程怎么利用代码实现临界资源的保护
5.4 多个进程怎么利用代码实现
- 面包店算法