操作系统(四)
作者:互联网
进程与线程
举个例子
在浏览网页的时候,可以看见在浏览器上首先出现的是网页中的文字部分,过了一段时间一些小的图片被显示出来,而那些更大的图片和动画则需要再过一段时间才能被渲染出来
在这个浏览器实现过程中,共启动四个线程,分别是获取数据的线程、显示文本的线程,解压图片的线程和渲染图片的线程。
如果没有线程,即只有一个进程来完成上述工作,效果又会是什么样子?如果只有一个进程,执行的代码必然是首先将页面布局、文本信息布局、图片对象等内容全部下载下来,然后逐个解码渲染最后再将所有信息全部整理好输出到屏幕上,结果就是对用户面对空白屏幕等待了较长时间,然后网页信息全部被显示出来
为什么用四个线程而不是四个进程来实现这样的浏览器呢?关键在于GetData,ShowText,ProcessImage,ShowImage是否要用到不同”进程资源“。Getdata将网络数据下载到内存缓存区,ShowText从内存缓冲区中读出数据并将其显示到屏幕上,显示此事不需要使用地址隔离策略将这两个内存缓存区分离到不同进程中,因此此处并不存在安全问题,而这种隔离导致内存的浪费和代码执行效率的降低。因此GetData,ShowText,ProcessImage,ShowImage是四个并发的指令序列,并使用共同地址空间等进程资源,所以是四个线程
void WebExplore()
{
char URL[] = "http://www.stanford.edu";
char buf[1024];
thread_creat(GetData,URL,buf);
thread_creat(ShowText,buf);
......
}
void Getdata(char *URL. char *buf)
{......}
void ShowText(char *buf)
{......}
用户级线程之间的切换
假设启动了两个用户级线程GetData和ShowText,当GetData下载网页文本数据时,GetData可以调用用户态函数Yield()让出CPU。此时Yield将会通过PC指针修改为300来切换到ShowText线程。在ShowText将文本数据显示到屏幕上以后,再调用用户态Yield()让出CPU,Yield又会再次修改PC指针为100,从而切换回GetData继续执行
显然,Yield()完成切换的核心函数。用一个更加具体的例子来说明Yield的工作过程,两个用户及线程,其中线程1执行A()函数,并再A()中调用B()函数;线程2执行C()函数,在函数中调用D()函数
首先线程1执行,进入函数A()执行,当要只从调用函数B()时,要通过栈来保存一些信息,以便在函数B()执行完成时再跳回A()执行,这是函数调用的基本知识。现在需要将返回地址104压栈;接下来进入函数B()执行。现在要跳到函数Yield()进行线程切换,所有204也会被压栈。现在开始执行Yield()函数中的代码,想一想这段代码功能是什么呢?先然时找到下一个线程以及下一个线程切换出去时的执行位置,然后跳转到这个位置。
Yield(){jmp300;}
继续执行,现在线程2执行,进入函数C(),要跳入函数D()前需要将地址304压栈并跳入D函数执行。现在要再次调用Yield()来切换线程,地址404压栈,然后执行Yield()函数。
不难想象此时的Yield应该为
Yield(){jmp 204;}
现在的问题是:从PC=204再往下执行回怎么样?不断地取指——执行,当执行到B()的“}”时会弹栈进行返回,栈中弹出的是地址404,跳到404执行。但是,应该挑战到104执行呀!
解决办法是分别用两个栈,如图所示
由于每个线程拥有自己的栈,所以再Yield()切换的时候不仅要修改PC的值,还要完成栈的切换。所以需要执行的Yield具体为
Yield(){tcb.esp=2004;esp=1004;}
当Yield切换到新的函数栈地址1004以后,Yield会遇到并执行“}”,这个“ret”指令弹出地址204给EIP。现在开始从地址204处,继续执行,当遇到B()中的“}”时会再一次弹栈返回,返回地址104继续执行,即返回A()函数执行。
用户级线程的创建
thread_creat(void * func)
{
long *stack=mallloc(SIZE_OF_USERSTACK)+SOME_SIZE
TCP *p = malloc(SIZE_OF_TCB);
*stack=func
*(stack--)=eax
......
p->esp=stack
}
对于下面一段代码
int s = 0;
int sum(int x, int y)
{
int t = x + y
s += t
}
为了了解sum函数时如何具体工作的,用gcc -S sum.c.
具体工作模式为:
(1)参数时被压入栈中进行传递的
(2)函数刚开始执行时,通过修改EBP寄存器到栈中去传递给该函数的擦拭农户
(3)也在函数的开始,通过保存原来的ebp以及修改后的ebp,使得当前ebp和原来ebp之间的内容构成函数执行的一个栈帧
(4)函数第一个修改的参数在ebp+8的地方,第二个参数放在ebp+12的地方,以此类推
(5)保存ebp,修改ebp以及通过ebp取出参数的代码时处理C语言函数调用时由编译器自动生成的,
对对应代码为
sum:
push %ebp
mov1 %esp,%ebp
move1 12(%ebp),%eap
add1 8(%ebp),%eax
add1 %eax,s
pop %ebp
re
内核级线程
用户级线程时完全在用户态内存中创建一个执行序列,即用户级线程的TCB,栈等内容都是创建在用户态中,操作系统完全不知道。现在呢合计线程就是要让内核态内存和用户态内存黑总创建一个执行序列
对于双核处理器,如果计算机系统中有两个用户及线程,由于操作系统并不知道存在两个指令序列,所以只能用到处理器中的一个核来执行其中的一个执行序列。即使调用Yield切换到下一个执行序列仍然只是利用一个核工作,双核处理器中另一个核一直空闲。如果计算机系统创建了两个个内核级线程,此时操作系统能操纵连个指令执行序列,会将该1分配给第一个执行序列,将核2分配给第二个执行序列。两个核可以同时“取指——执行”,硬件工作效率得到显著提升。
对于双核结构,如果计算机系统中有两个进程,虽然两个进程对应的两个执行序列都可以被操作系统感知,但对应于两个进程的两个执行序列并不适合并行的放在多核处理器的多核上执行。这是因为多核处理器中多核处理器中的多个核通常共享存储管理部件以及一些缓存等,为避免进程之间的影响,进程之间应该做到地址隔离,即每个进程使用自己的地址空间和地址映射表。硬件MMU就是用来查找映射表的硬件,而某些缓存就用来暂存一些最近的地址映射结果。
内核级线程的切换
回顾用户级线程的切换,主要分为三步:TCB切换,根据TCB中存储的栈指针完成用户栈切换,根据用户栈压入函数返回地址完成完成PC指针切换
不难想象,内核级线程的切换也要完成“TCB切换,栈切换,PC指针切换”三件事,那么它和用户级线程的区别在哪里呢?内核级线程的TCB存储在操作系统内核中,因此完成TCB切换的程序应该执行在操作系内核中。这是第一个重要区别,即用户及线程通过调用用户态Yield()完成切换,而内核级线程切换的故事应该从进入内核——中断开始,因为中断会导致从用户态到内核态的切换
由于进入内核态才能完成内核级线程的切换,所以要在内核的某个地方完成PC指针切换。仿照用户级线程,这个PC指针也应该放在栈中,利用栈完成切换。对于内核级线程,这个栈应该时内核栈,首先切换内核栈,然后引发PC指针切换。因此和用户级线程相比,内核级线程的第二个重要区别是切换栈要同时切换内核栈和用户栈。
综上所述,用户级线程切换的核心思想时根据存放在哦那个胡程序中的TCB找到用户栈,通过用户栈切换完成用户线程的切换,整个切换过程的切换,整个切换过程通过调用Yield函数引发。内核级线程切换的核心是首先进入操作内核中找到线程TCB,根据TCB找到线程的内核栈,通过内核栈切换完成内核级线程的切换,整个切换过程由中断引发
首先需要弄明白中断以后会发生了什么。
执行指令int/iret指令执行时,会找到当前进程的内核栈,然后将用户态执行的一些重要信息,如当前程序执行位置CS:EIP、当前哦那个胡站栈顶位置SS:ESP以及标志寄存器FLAG压到内核栈中,实际上,所有外部中断,比如时钟中断,键盘中断,磁盘读写完成中断等,都会引发上述动作。而iret指令正好是Int指令的逆过程
可以分为五个阶段:
(1)中断进入,就是int指令或其他硬件中断的中断处理入口,核心工作时要记录当前程序在用户态执行时的信息,如当前使用的用户栈,当前程序执行位置、当前执行的线程信息等。其中用户栈地址SS:ESP和PC指针信息CS:EIP已经由中断处理硬件压入当前线程对应的内核栈中了,只有当前的执行现场信息还没有保存。所以在进入中断处理程序的开始需要编写代码保护用户态程序当前执行现场。此处以“int 0x80”为例,应该在中断处理程序system_call的开始出执行下面的代码,即中断进入代码:
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
以上代码用于保护用户态程序执行现场,接下来就可以使用这些寄存器来执行内核态处理程序了,如用户“movel $0x10,%edx”,“movel %dx,%ds,“movel %dx,%es”来江段寄存器DS,ES设置为内核数据段选择子,这样以后再访问的数据就是内核数据了
(2)调用schedule函数,引起TCB切换。在中断处理程序中,如果当前线程启动了磁盘读写等操作,即发现当前线程应该让出CPU时,系统啮合就会调用schudule函数来完成TCB的切换。具体做法很简单,例如在向当前磁盘发出读写指令之后,将当前线程的状态修改为阻塞,具体代码实现为cuurent->state=拥塞,并将current添加一个等待某个磁盘读写完成的等待对列联表上。接下来调用schedule实现TCB切换
为了完成TCB的切换,schdule函数首先从就绪队列中选取出下一个要执行线程的TCB。找到下一个TCB之后,此处用next指针指向这个TCB,利用current和next指针指向的信息就可以开始内核级线程切换的第三阶段了。
(3)内核栈的切换,具体来说,就是将当前的ESP寄存器放在current指向的TCB中,再从next指向的TCB中取出esp字段赋值给ESP寄存器。由于现在执行再内核态,所以当前寄存器ESP指向的就是当前线程的内核栈,而放在TCB中esp也是线程的内核地址,所以切换时内核栈的切换
current->esp=esp;
esp=next->esp
(4)第四段是中断返回,这是要为内核级线程切换的最后一个阶段“用户栈切换”做准备,同时也和内核级线程第一阶段“中断进入”相对应。在这一阶段中,要将存储在下一个线程的内核栈中的用户程序执行现场恢复出来,这个现场时这个线程在切换出去时由中断程序入口保存的。
pop1 %ebx
pop1 %ecx
pop1 %edx
pop1 %fs
pop1 %es
pop1 %ds
(5)用户栈切换,实际上就是切换用户态程序PC指针以及相应的用户栈,即需要将CS:EIP寄存器设置为当前哦那个胡程序执行地址,将SS:ESP寄存器设置为当前用户栈地址即可,而这两个信息现在就可以在下一个线程的内核栈中,只要执行iret指令就可以完成这个切换了
内核级线程的创建
内核级线程的切换主要由四个具体的切换构成:切换TCB,切换内核栈,切换用户栈,用户程序PC指针切换。相应的没创建内核级线程的关键时初始化TCB,内核栈和用户栈。具体来说:第一,创建一个TCB,主要存放内核栈的esp指针;第二,分配一个内核栈,其中主要存放用户程序PC指针、用户栈地址以及执行现场;第三、主要存放进入用户态函数时用到的参数
*krnstack=用户栈的段选择子(即SS)
*(krnstack-4)=用户栈的偏移
*(krnstack-8)=eflags
*(krnstack-12)=用户栈代码的段选择子
*(krnstack-16)=用户程序入口地址
标签:操作系统,用户,线程,内核,执行,TCB,切换 来源: https://blog.csdn.net/weixin_43591948/article/details/118684521