网站开发的技术类型有哪些,wordpress移动端分享,网站开发的课程,无锡网站建设818gx初始时#xff0c;CPU的执行流为进程#xff1b;当产生了线程概念后#xff0c;CPU执行流变为了线程#xff0c;大大增大了一个周期以内进程的执行速度。
线程产生的作用就是为了提速#xff0c;利用线程提速#xff0c;原理就是实现多个执行流的伪并行#xff0c;让处…初始时CPU的执行流为进程当产生了线程概念后CPU执行流变为了线程大大增大了一个周期以内进程的执行速度。
线程产生的作用就是为了提速利用线程提速原理就是实现多个执行流的伪并行让处理器多执行自己进程中的代码。若进程只占用处理器的一个时间片那将进程细分为线程后一个进程中的多个线程可同时占用处理器。类比拼车与不可拼车两种模式CPU相当于一辆车拼车表示开启线程允许拼车后用户的出行成本就大大降低了。
Ⅰ.程序、进程、线程关系
程序是指静态的、存储在文件系统上、尚未运行的指令代码它是实际运行时程序的映像。
进程是指正在运行的程序即进行中的程序程序必须在获得运行所需要的各类资源后才能成为进程资源包括进程所使用的栈使用的寄存器等。
进程线程资源线程自己基本上不拥有系统资源只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
1.进程线程区别
进程是资源分配的基本单位线程是处理器调度的基本单位。进程拥有自己独立的地址空间每启动一个进程系统为其分配地址空间建立数据表来维护代码段、堆栈段和数据段线程没有自己的地址空间需要借助进程的资源“生存”。CPU切换线程的开销比进程小。创建线程的资源开销比进程小。线程之间通信更方便同一个进程下线程共享全局变量静态变量等数据进程之间的通信需要以通信的方式IPC进行。多进程程序更安全生命力更强一个进程死掉不会对另一个进程造成影响源于有独立的地址空间多线程程序更不易维护一个线程死掉可能整个进程就死掉了因为共享地址空间。进程对资源保护要求高开销大效率相对较低线程资源保护要求不高但开销小效率高可频繁切换。
2.线程进程状态
初始态、就绪态、运行态、阻塞态、终止态 3.进程的身份证-PCB
针对多任务处理系统中任务切换执行存在的如下疑问
(1要加载一个任务上处理器运行任务由哪来也就是说调度器从哪里才能找到该任务进程表 (2即使找到了任务任务要在系统中运行其所需要的资源从哪里获得PCB中的寄存器和栈等资源 (3即使任务已经变成进程运行了此进程应该运行多久呢总不能让其独占处理器吧。时间片 (4即使知道何时将其换下处理器那当前进程所使用的这一套资源寄存器内容应该存在哪里?PCB最顶层的寄存器映像 (5进程被换下的原因是什么下次调度器还能把它换上处理器运行吗取决于PCB的状态 (6前面都说过了进程独享地址空间它的地址空间在哪里 PCB-页表
……
为解决以上问题操作系统为每个进程提供了一个 PCBProcess Control Block即程序控制块它就是进 程的身份证用它来记录与此进程相关的信息比如进程状态、PID、优先级等。
每个进程都有自己的 PCB所有 PCB 放到一张表格中维护这就是进程表调度器可以根据这张表选择上处理器运行的进程 。 PCB的栈为进程所使用的 0 特权级下内核栈寄存器映像存储的也是内核态的寄存器栈指针对应的是内核态下的寄存器映像地址即内核态栈地址。
3.实现线程的两种方式一一内核或用户进程
线程仅仅是个执行流在用户空间还是在内核空间实现它最大的区别就是线程表在哪里由谁来调度它上处理器 。 如果线程在用户空间中实现线程表就在用户进程中用户进程就要专门写个线程用作线程调度器由它来调度进程内部的其他线程。如果线程在内核空间中实现线程表就在内核中该线程就会由操作系统的调度器统一调度无论该线程属于内核还是用户进程。
1内核中实现线程
线程机制由内核完成。
好处
相比在用户空间中实现线程内核提供的线程相当于让进程多占了处理器资源当进程中的某一线程阻塞后 由于线程是由内核空间实现的操作系统认识线程所以就只会阻塞这一个线程此线程所在进程内的其他线程将不受影响
缺点
用户进程需要通过系统调用陷入内核这多少增加了 一些现场保护的栈操作这还是会消耗一些处理器时间
2用户程序中实现线程
**线程机制由用户程序通过标准库完成。**处理器依旧按照进程作为执行流执行程序。
好处
可移植性强在不支持线程的操作系统上也可以写出完美支持线程的用户程序。线程的调度算法是由用户程序自己实现的可以根据实现应用情况为某些线程加权调度。将线程的寄存器映像装载到 CPU 时可以在用户空间完成即不用陷入到内核态这样就免去了进入内核时的入栈和出栈操作。
缺点
进程中的某个线程若出现了阻塞通常是由于系统调用造成的操作系统不知道进程中存在线程它以为此进程是传统型进程单线程进程因此会将整个进程挂起即进程中的全部线程都无法运行。如果在用户空间中实现线程但凡进程中的某个线程开始在处理器上执行后只要该线程不主动让出处理器此进程中的其他线程都没机会运行。 只能凭借开发人员“人为”地在线程中调用类似 pthread_yield 或 pthread_exit 之类的方法使线程让出处理器使用权此类方法通过回调方式触发进程内的线程调度器让调度器有机会选择进程内的其他线程上处理器运行。进程在一个时间片内既要完成资源分配又要处理线程调度的工作导致提速效果较差。
如果在用户空间中实现线程用户线程就要肩负起调度器的责任因此除了要实现进程内的线程调度器外还要自己在进程内维护线程表 导致开销很大。 Ⅱ.在内核空间实现线程
包括主线程和新建立的线程两种方式。 call 指令属于“有去有回”的指令它在“去”之前先在栈中进入被调函数时的栈顶处〉留下返回地址它的“回”则需要在 ret 指令的配合下才能完成 ret 将楼顶的值当作 call 留下的返回地址在保证栈顶值正确的情况下 ret 能把处理器重新带回到主调函数中。
1.线程执行过程
1.构建就绪线程队列和所有线程队列
struct task_struct* main_thread; // 主线程 PCB
struct list* thread_ready_list; // 就绪队列
struct list* thread_all_list; // 所有线程
static struct list_elem* thread_tag; // 用于保存队列中的线程结点队列中存储的是PCB标志位general_tag和all_list_tag分别表示就绪线程队列标志和所有线程队列标志。 /* 根据结构体成员找到结构体地址 */
// 计算偏移量建立一个起始地址为0的虚拟结构体对成员取地址就是偏移量
#define offset(struct_type, member) (int)(((struct_type*)0)-member)
// 结构体入口地址当前成员变量地址-当前成员变量的偏移地址
#define elem2entry(struct_type, struct_member_name, elem_ptr) \(struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))2.申请一页大小的PCB内核空间
struct task_struct* thread get_kernel_pages(l); 3.初始化线程PCB
1定义PCB结构
/* 进程控制块 */
struct task_struct{uint32_t *self_kstack; // 线程的内核栈enum THREAD_STATUS status; // 线程状态uint32_t priority; // 线程优先级char name[16];// 此任务自上 cpu 运行后至今占用了多少 cpu 嘀嗒数也就是此任务执行了多久uint32_t elapsed_ticks;// general_tag 的作用是用于线程在一般的队列中的结点struct list_elem general_tag;// all_list_tag 的作用是用于线程队列 thread_all_ list 中的结点struct list_elem all_list_tag;uint32_t* pgdir; // 进程自己页表的虚拟地址uint32_t stack_magic; // 线程栈的边界标记用于标记栈是否溢出
};内核栈。存储线程执行的函数、传参、寄存器、内存地址等信息 任务的时间片。每次时钟中断都会将当前任务的 ticks 减1 当减到 0时就被换下处理器。 优先级。priority 表示任务的优先级咱们这里优先级体现在任务执行的时间片上即优先级越高每次任务被调度上处理器后执行的时间片就越长。 general_tag。线程的标签 当线程被加入到就绪队列也thread_ready_list 或其他等待队列中时就把该线程 PCB 中 general_tag 的地址加入队列。 all_list_tag。在所设计的系统中 为管理所有线程还存在一个全部线程队列thread_all_list因此线程还需要另外一个标签即 all_list_tag。专用于线程被加入全部线程队列时使用。 这两个标签仅仅是加入队列时用的将来从队列中把它们取出来时还需要再通过 offset 宏与 elem2entry宏的“反操作“实现从general_tag 到thread 的地址转换将它们还原成线程的 PCB 地址后才能使用。 pgdir。任务自己的页表 。如果该任务为线程 pgdir 则为 NULL否则 pgdir会被赋予页表的虚拟地址注意此处是虚拟地址页表加载时还是要被转换成物理地址的 。
如何实现就绪队列的标签到PCB转换的过程呢通过上面的offset函数和elem2entry函数
2初始化PCB
/* 初始线程PCB */
void init_thread(struct task_struct* pthread, char* name, uint32_t proi){memset(pthread,0, sizeof(*pthread));strcpy(pthread-name, name);if (pthread main_thread){pthread-status TASK_RUNNING;}else{pthread-status TASK_READY;}// 初始化在线程PCB的最顶端栈向下生长// pthread在分配了一页内存后指向PCB的最底端加上PG_SIZE即为PCB最顶端地址pthread-self_kstack (uint32_t*) ((uint32_t)pthread PG_SIZE);pthread-priority proi;pthread-ticks proi;pthread-elapsed_ticks 0;pthread-pgdir NULL;pthread-stack_magic 0x19870916; // 自定义魔数
}分配栈空间
初始化PCB中优先级、时间片、状态
3.初始化线程栈
1定义线程栈
线程栈结构由于线程中断后再次执行需要恢复执行的函数、参数以及对应的寄存器信息因此建立栈维护线程现场
/* 建立线程自己的栈 * 线程自己的栈用于存储线程中待执行的函数* 此结构在线程自己的内核梭中位置不固定* 仅用在 switch_to 时保存线程环境。* 实际位置取决于实际运行情况。
*/
struct thread_stack{uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;/* 线程第一次执行时 eip 指向待调用的函数 kernel_thread其他时候 eip 是指向 switch_ to 的返回地址*/void (*eip) (thread_func* func, void* func_arg);/*** 以下仅供第一次被调度上 cpu 时使用 ****/void (*unused_retaddr); // 参数 unused_retaddr 只为占位置充数为返回地址thread_func* funciton; // 自 kernel_thread 所调用的函数名void* func_arg; // 由 kernel_thread 所调用的函数所需的参数
};2初始化线程栈
/* 初始化线程栈 */
void thread_creat(struct task_struct* pthread, thread_func* function, void* func_arg){// 先预留出中断栈空间pthread-self_kstack - sizeof(struct intr_stack);// 预留出线程栈空间pthread-self_kstack - sizeof(struct thread_stack);// 设置线程栈起始地址struct thread_stack* kthread_stack (struct thread_stack*) pthread-self_kstack;kthread_stack-funciton function;kthread_stack-func_arg func_arg;kthread_stack-eip kernel_thread;kthread_stack-ebp kthread_stack-ebx kthread_stack-edi kthread_stack-esi 0;
}4.将创建的线程添加到就绪线程队列和所有线程队列中 /* 确保之前不在就绪队列中 */ASSERT(!(elem_find(thread_ready_list, thread-general_tag)));list_append(thread_ready_list, thread- general_tag);/* 确保之前不在所有队列中 */ASSERT(!(elem_find(thread_ready_list, thread-all_list_tag)));list_append(thread_ready_list, thread- all_list_tag);5.设置kernel中main函数为主线程
main函数在启动之后会自动建立线程栈我们在kernel.S中为其分配了PCB空间并未初始化PCB因此需要对其进行初始化 。
1获取当前esp指针赋值给main_thread的PCB
/* 获取当前线程 pcb 指针 */
struct task_struct* running_thread(){uint32_t esp;asm(mov %%esp, %0:g(esp));/* 取 esp 整数部分即 pcb 起始地址*/return (struct task_struct*) (esp Oxfffff000);
}2完善main_threadPCB信息
static void make_main_thread(void){// 定义main_thread的PCB地址main_thread running_thread();// 初始化名字和优先级init_thread(main_thread, main, 31);// 由于main程序已经在运行了因此只需要放入all_list即可ASSERT(!elem_find(thread_all_list, main_thread-all_list_tag));list_append(thread_all_list, main_thread-all_list_tag);
}Ⅲ.任务调度器和任务切换
1.任务调度器工作过程
线程调度器主要任务就是读写就绪队列增删里面的结点结点是线程 PCB 中的 general_tag“相当于”线程的 PCB从队列中将其取出时一定要还原成 PCB 才行 。
当前任务执行的状态什么时候结束ticks0结束了之后如何完成保护现场操作结束了如何找到下一个线程thread_ready_list如何恢复下一个线程的现场switch_to
(1)PCB的ticks决定了任务执行的时间系统设定当ticks减为0时当前任务被换下处理器根据线程的运行状态决定线程是否加入就绪队列
TASK-RUNNING。将当前队列加入thread_ready_list队尾并将ticks设置为prio。others。不对当前线程进行任何操作。
故需要设定时钟中断系统。
(2)调度器按照队列先进先出的顺序把就绪队列中的第 1 个结点作为下一个要运行的新线程将该线程的状态置为 TASK_RUNNING之后通过函数 switch_to 将新线程的寄存器环境恢复新线程便开始执行 。
1完整调度过程的3个步骤
时钟中断处理函数调度器schedule任务切换函数switch_to
2PART1-注册时钟中断处理函数
Intel处理器支持256个中断在前面的kernel.S中通过中断向量号调用中断处理程序数组 idt_table 中的 C 版本的处理程序就是文件 kemel.S 中代码 call [idt_table %1 *4的作用。由于idt_table存储的就是中断处理程序因此为设备注册中断处理程序的工作变得很简单我们不用去修改中断描述符直接把中断向量作为数组下标去修改 idt_table中断向量数组元素即可。 for (i 0; i IDT_DESC_CNT; i) {/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,* 见kernel/kernel.S的call [idt_table %1*4] */idt_table[i] general_intr_handler; // 默认为general_intr_handler。// 以后会由register_handler来注册具体处理函数。intr_name[i] unknown; // 先统一赋值为unknown }之前的时钟中断处理函数还是用通用的函数来处理的 即 general_intr_handler此函数作为默认的中断处理函数即某个中断源没有中断处理程序时才用它来代替。
2.1改进的通用中断处理函数general_intr_handler 由于需要打印输出中断信息为防止由于光标错误值引发异常加入了set_cursor()光标位置设置函数 为保证中断调用出现的缺页异常能及时被发现和处理设置缺页中断号14判定函数标定缺页异常 加了 Pagefault 的处理。 Pagefault 就是通常所说的缺页异常它表示虚拟地址对应的物理地址不存在也就是虚拟地址尚未在页表中分配物理页这样会导致 Pagefault 异常。导致 Pagefault 的虚拟地址会被存放到控制寄存器 CR2 中我们加入的内联汇编代码就是让 Pagefault 发生时将寄存器 cr2 中的值转储到整型变量 page_fault_vaddr 中并通过 put_str函数打印出来。因此如果程序运行过程中出现异常 Pagefault 时将会打印出导致 Pagefault 出现的虚拟地址。
/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {if (vec_nr 0x27 || vec_nr 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚保留return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。}/* 将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */set_cursor(0);int cursor_pos 0;while(cursor_pos 320) {put_char( );cursor_pos;}set_cursor(0); // 重置光标为屏幕左上角put_str(!!!!!!! excetion message begin !!!!!!!!\n);set_cursor(88); // 从第2行第8个字符开始打印put_str(intr_name[vec_nr]);if (vec_nr 14) { // 若为Pagefault,将缺失的地址打印出来并悬停int page_fault_vaddr 0; asm (movl %%cr2, %0 : r (page_fault_vaddr)); // cr2是存放造成page_fault的地址put_str(\npage fault addr is );put_int(page_fault_vaddr); }put_str(\n!!!!!!! excetion message end !!!!!!!!\n);// 能进入中断处理程序就表示已经处在关中断情况下,// 不会出现调度进程的情况。故下面的死循环不会再被中断。while(1);
}2.2注册时钟中断处理函数
……uint32_t ticks;
……
static void intr_timer_handler(void){struct task_struct* cur_thread running_thread();ASSERT(cur_thread-stack_magic 0x19870916);cur_thread-elapsed_ticks; // 记录占用 CPU的时间ticks; // 从内核开始处理第一次中断后开始至今的滴答数if(cur_thread-ticks 0){schedule();}else{cur_thread-ticks--;}
}void timer_init(){put_str(timer init);// 设置 8253 的定时周期也就是发中断的周期 frequency_set( CNTRERO_PORT, \COUNTERO_NO, \READ_WRITE_LATCH, \COUNTER MODE, \COUNTERO_VALUE);// 注册时钟中断函数register_handler(0x20, intr_timer_handler);put_str(timer_init done\n) ;
}2.3中断注册函数
/* 中断处理程序数组第 vector_no 个元素中
注册安装中断处理程序 function */
void register_handler(uint32_t vec_no, intr_handler* function){/* idt_table 数组中的函数是在进入中断后根据中断向量号调用的* 见 kernel/kernel.S 的 call [idt_table%1*4] */idt_table[vec_no] function;
}3PART2-调度器schedule
/* 实现任务调度 */
void schedule(){// 系统关中断下进行ASSERT(intr_get_status INTR_OFF);struct task_struct *cur_thread running_thread();// 若此线程只是 cpu 时间片到了将其加入到就绪队列尾if(cur_thread-status TASK_RUNNING){ASSERT(thread_ready_list, cur_thread-general_tag);list_append(thread_ready_list, cur_thread-general_tag);// 重新将当前线程的 ticks 再重置为其 prioritycur_thread-ticks cur_thread-priority;cur_thread-status TASK_READY;}else{/* 若此线程需要某事件发生后才能继续上 cpu 运行不需要将其加入队列因为当前线程不在就绪队列中 */}ASSERT(list_empty(thread_ready_list));/* 从就绪队列取出下一个就绪线程*/// 清空线程节点thread_tag NULL;thread_tag list_pop(thread_ready_list);struct task_struct *next_thread elem2entry(struct task_struct, general_tag, thread_tag);next_thread-status TASK_RUNNING;// 载入寄存器switch_to(cur_thread, next_thread);
}判断当前线程是否需要继续执行决定是否重新加入就绪队列从就绪队列pop出下一个线程switch_to载入新线程寄存器组
要求在关中断下进行
4PART3-任务切换函数switch_to
任务切换的过程包括保存当前任务上下文执行中断处理程序保存当前中断处理程序上下文执行下一个任务因此
需要保存任务的上下文既需要保存中断发生时任务的寄存器、栈状态同时也要保存内核中任务的还未执行的环境。具体包括两部分 (1上下文保护的第一部分负责保存任务进入中断前的全部寄存器目的是能让任务恢复到中断前 。
通过kernel.S完成中断前的保存工作以及中断处理程序跳转执行工作
extern idt_table ;idt_table是C中注册的中断处理程序数组%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序自己知道自己的中断向量号是多少%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境push dspush espush fspush gspushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI mov al,0x20 ; 中断结束命令EOIout 0xa0,al ; 向从片发送out 0x20,al ; 向主片发送push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便call [idt_table %1*4] ; 调用idt_table中的C版本中断处理函数jmp intr_exitsection .datadd intr%1entry ; 存储各个中断入口程序的地址形成intr_entry_table数组
%endmacrosection .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境add esp, 4 ; 跳过中断号popadpop gspop fspop espop dsadd esp, 4 ; 跳过error_codeiretd(2上下文保护的第二部分负责保存这 4 个寄存器 esi 、 edi 、 ebx 和 ebp 目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码保证顺利走到退出中断的出口利用第一部分保护的寄存器环境彻底恢复任务。
[bits 32]
section .text
global switch_to
switch_to:;栈中此处是返回地址 push esipush edipush ebxpush ebpmov eax, [esp 20] ; 得到栈中的参数cur, cur [esp20]mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,; self_kstack在task_struct中的偏移为0,; 所以直接往thread开头处存4字节便可。
;------------------ 以上是备份当前线程的环境下面是恢复下一个线程的环境 ----------------mov eax, [esp 24] ; 得到栈中的参数next, next [esp24]mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针pop ebppop ebxpop edipop esiret ; 返回到上面switch_to下面的那句注释的返回地址,; 未由中断进入,第一次执行时会返回到kernel_thread5PART4-启用线程调度
系统初始化函数中加入thread_init()函数在main函数中加入thread_start()函数。
开启执行……
总结
pushad 本指令将EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 这8个32位通用寄存器依次压入堆栈其中SP的值是在此条件指令未执行之前的值压入堆栈之后ESP-32–ESP。
popad本指令依次弹出堆栈中的32位字到 EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX中弹出堆栈之后ESP32–ESP。