第九章-线程

news2024/9/23 3:32:09

初始时,CPU的执行流为进程;当产生了线程概念后,CPU执行流变为了线程,大大增大了一个周期以内进程的执行速度。

线程产生的作用就是为了提速,利用线程提速,原理就是实现多个执行流的伪并行,让处理器多执行自己进程中的代码。若进程只占用处理器的一个时间片,那将进程细分为线程后,一个进程中的多个线程可同时占用处理器。类比拼车与不可拼车两种模式,CPU相当于一辆车,拼车表示开启线程,允许拼车后,用户的出行成本就大大降低了。

Ⅰ.程序、进程、线程关系

程序是指静态的、存储在文件系统上、尚未运行的指令代码,它是实际运行时程序的映像。

进程是指正在运行的程序,即进行中的程序,程序必须在获得运行所需要的各类资源后才能成为进程,资源包括进程所使用的栈,使用的寄存器等。

进程=线程+资源,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

1.进程线程区别
  1. 进程是资源分配的基本单位,线程是处理器调度的基本单位。
  2. 进程拥有自己独立的地址空间,每启动一个进程,系统为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有自己的地址空间,需要借助进程的资源“生存”。
  3. CPU切换线程的开销比进程小。
  4. 创建线程的资源开销比进程小。
  5. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行。
  6. 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间);多线程程序更不易维护,一个线程死掉,可能整个进程就死掉了(因为共享地址空间)。
  7. 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换。
2.线程进程状态

初始态、就绪态、运行态、阻塞态、终止态

在这里插入图片描述

3.进程的身份证-PCB

针对多任务处理系统中,任务切换执行存在的如下疑问:

(1)要加载一个任务上处理器运行,任务由哪来?也就是说,调度器从哪里才能找到该任务?进程表
(2)即使找到了任务,任务要在系统中运行,其所需要的资源从哪里获得?PCB中的寄存器和栈等资源
(3)即使任务已经变成进程运行了,此进程应该运行多久呢?总不能让其独占处理器吧。时间片
(4)即使知道何时将其换下处理器,那当前进程所使用的这一套资源(寄存器内容)应该存在哪里?PCB最顶层的寄存器映像
(5)进程被换下的原因是什么?下次调度器还能把它换上处理器运行吗?取决于PCB的状态
(6)前面都说过了,进程独享地址空间,它的地址空间在哪里? PCB-页表

……

为解决以上问题,操作系统为每个进程提供了一个 PCB,Process 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 才行 。

  • 当前任务执行的状态,什么时候结束?ticks===0
  • 结束了之后,如何完成保护现场操作?
  • 结束了如何找到下一个线程?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个步骤
  1. 时钟中断处理函数
  2. 调度器schedule
  3. 任务切换函数switch_to
(2)PART1-注册时钟中断处理函数

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;
}
(3)PART2-调度器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 再重置为其 priority
        cur_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);
}
  1. 判断当前线程是否需要继续执行,决定是否重新加入就绪队列
  2. 从就绪队列pop出下一个线程
  3. switch_to载入新线程寄存器组

要求在关中断下进行

(4)PART3-任务切换函数switch_to

任务切换的过程包括:保存当前任务上下文;执行中断处理程序,保存当前中断处理程序上下文;执行下一个任务,因此

需要保存任务的上下文,既需要保存中断发生时任务的寄存器、栈状态,同时也要保存内核中任务的还未执行的环境。具体包括两部分:

在这里插入图片描述

(1)上下文保护的第一部分负责保存任务进入中断前的全部寄存器,目的是能让任务恢复到中断前 。

通过kernel.S完成中断前的保存工作,以及中断处理程序跳转执行工作

extern idt_table		 ;idt_table是C中注册的中断处理程序数组

%macro VECTOR 2
section .text
intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少

   %2				 ; 中断若有错误码会压在eip后面 
; 以下是保存上下文环境
   push ds
   push es
   push fs
   push gs
   pushad			 ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

   ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
   mov al,0x20                   ; 中断结束命令EOI
   out 0xa0,al                   ; 向从片发送
   out 0x20,al                   ; 向主片发送

   push %1			 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
   call [idt_table + %1*4]       ; 调用idt_table中的C版本中断处理函数
   jmp intr_exit

section .data
   dd    intr%1entry	 ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro

section .text
global intr_exit
intr_exit:	     
; 以下是恢复上下文环境
   add esp, 4			   ; 跳过中断号
   popad
   pop gs
   pop fs
   pop es
   pop ds
   add esp, 4			   ; 跳过error_code
   iretd

(2)上下文保护的第二部分负责保存这 4 个寄存器 esi 、 edi 、 ebx 和 ebp ,目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码,保证顺利走到退出中断的出口,利用第一部分保护的寄存器环境彻底恢复任务。

[bits 32]
section .text
global switch_to
switch_to:
   ;栈中此处是返回地址	       
   push esi
   push edi
   push ebx
   push ebp

   mov eax, [esp + 20]		 ; 得到栈中的参数cur, cur = [esp+20]
   mov [eax], esp                ; 保存栈顶指针esp. task_struct的self_kstack字段,
				 ; self_kstack在task_struct中的偏移为0,
				 ; 所以直接往thread开头处存4字节便可。
;------------------  以上是备份当前线程的环境,下面是恢复下一个线程的环境  ----------------
   mov eax, [esp + 24]		 ; 得到栈中的参数next, next = [esp+24]
   mov esp, [eax]		 ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
				 ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
   pop ebp
   pop ebx
   pop edi
   pop esi
   ret				 ; 返回到上面switch_to下面的那句注释的返回地址,
				 ; 未由中断进入,第一次执行时会返回到kernel_thread

在这里插入图片描述

(5)PART4-启用线程调度

系统初始化函数中加入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中,弹出堆栈之后,ESP+32–>ESP。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1080034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3前端开发系列 - electron开发桌面程序(2023-10月最新版)

文章目录 1. 说明2. 创建项目3. 创建文件夹electron3.1 编写脚本electron.js3.2 编写脚本proload.js 4. 修改package.json4.1 删除type4.2 修改scripts4.3 完整的配置如下 5. 修改App.vue6. 修改vite.config.ts7. 启动8. 打包安装9. 项目公开地址 1. 说明 本次安装使用的环境版…

提取log文件中的数据,画图

要提取的log格式如下&#xff1a; 代码如下&#xff1a; import reimport matplotlib.pyplot as plt import numpy as npimport argparse from os import path from re import searchclass DataExtractor(object): DataExtrator class def __init__(self, infile, keyword, out…

电脑上播放4K视频需要具备哪些条件?

在电视上播放 4K&#xff08; 4096 2160 像素&#xff09;视频是很简单的&#xff0c;但在电脑设备上播放 4K 视频并不容易。相反&#xff0c;它们有自己必须满足的硬件要求。 如果不满足要求&#xff0c;在电脑上打开 4K 分辨率文件或大型视频文件会导致卡顿、音频滞后以及更…

ROS中的命名空间

ROS中的节点、参数、话题和服务统称为计算图源&#xff0c;其命名方式采用灵活的分层结构&#xff0c;便于在复杂的系统中集成和复用。以下是一些命名的示例&#xff1a; /foo /stanford/robot/name /wg/node1计算图源命名是ROS封装的一种重要机制。每个资源都定义在一个命名空…

微信小程序wxml使用过滤器

微信小程序wxml使用过滤器 1. 新建wxs2. 引用和使用 如何在微信小程序wxml使用过滤器&#xff1f; 犹如Angular使用pipe管道这样子方便&#xff0c;用的最多就是时间格式化。 下面是实现时间格式化的方法和步骤&#xff1a; 1. 新建wxs 插入代码&#xff1a; /*** 管道过滤工…

泡泡玛特,难成“迪士尼”

作者 | 艺馨 豆乳拿铁 排版 | Cathy 监制 | Yoda 出品 | 不二研究 新增长难寻&#xff0c;新故事难讲。泡泡玛特(06682.HK)业绩增长承压的困局&#xff0c;都写在最新的半年报里。 曾经潮玩领域的王者、“潮玩第一股”泡泡玛特&#xff0c;主题城市乐园于9月26日在北京朝阳…

centos下安装配置redis7

1、找个目录下载安装包 sudo wget https://download.redis.io/release/redis-7.0.0.tar.gz 2、将tar.gz包解压至指定目录下 sudo mkdir /home/redis sudo tar -zxvf redis-7.0.0.tar.gz -C /home/redis 3、安装gcc-c yum install gcc-c 4、切换到redis-7.0.0目录下 5、修改…

2023年中国医学影像信息系统市场规模、竞争格局及行业趋势分析[图]

医学影像信息系统简称PACS&#xff0c;与临床信息系统、放射学信息系统、医院信息系统、实验室信息系统同属医院信息系统。医学影像信息系统是处理各种医学影像信息的采集、存储、报告、输出、管理、查询的计算机应用程序。主要包括&#xff1a;预约管理、数据接收、影像处理、…

[读博随笔] 系统安全和论文写作的那些事——不忘初心,江湖再见

很难想象读博这四年的时光意味着什么&#xff0c;是对妻子和儿子深切的思念。我在珞珈山下挑灯夜读&#xff0c;你在贵阳家中独自照顾幼子。怕的不是孑然一身&#xff0c;而是明明已经习惯两个人&#xff0c;又必须各自前行&#xff0c;像单打独斗的勇士。想到千里之外还有一个…

【软考设计师】【计算机系统】E01 计算机硬件组成与CPU

【计算机系统】E01 计算机硬件组成与CPU 硬件组成概述中央处理单元 CPUCPU 组成运算器控制器寄存器组 多核 CPU 硬件组成概述 运算器&#xff1a; 数据加工处理部件&#xff0c;用于完成计算机的各种算术和逻辑运算。控制器&#xff1a; 顾名思义&#xff0c;控制整个CPU的工作…

2023年中国牙线市场规模、竞争现状及行业需求前景分析[图]

牙线是由合成纤维或其他材料制成&#xff0c;或添加香料、色素、活性成分等&#xff0c;用来清洁牙齿邻面附着物的线。能够有效包裹牙齿&#xff0c;对于清洁平面/凸起牙面和牙齿邻接面的牙菌斑效果很好&#xff0c;还可以实现对于牙缝间食物/异物的剔除&#xff0c;有效清洁口…

水库大坝除险加固安全监测系统解决方案

一、系统背景 为贯彻落实《办公厅关于切实加强水库除险加固和运行管护工作的通知》&#xff08;〔2021〕8号&#xff09;要求&#xff0c;完成“十四五”小型病险水库除险加固、雨水情测报和大坝安全监测设施建设任务&#xff0c;规范项目管理&#xff0c;消除安全隐患&#xf…

nodejs+vue 教学辅助管理系统

通过科技手段提高自身的优势&#xff1b;对于驾校预约管理系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了驾校预约管理系统&#xff0c;它彻底改变了过去传统的管理方式&#xff0c;不仅使服务管理难度变低了&#xff0c;还提升了管理的灵活性。…

云里雾里?云方案没有统一标准,业务结合实际情况才是应该着重考虑的

公共云正在迅速成为IT领导者及其业务线同行的首选平台。根据研究机构IDC的数据&#xff0c;2018年全球公共云服务和基础设施支出预计将达到1600亿美元&#xff0c;比2017年的投资水平增长23%。 然而&#xff0c;在向按需IT转变的步伐不断加快的同时&#xff0c;首席信息官们面…

文旅如何以数字人三维动画宣传片,实现文化资源数字化转化?

近日&#xff0c;山西文旅推出虚拟星推官——晋依依&#xff0c;通过数字人三维动画宣传片的形式&#xff0c;以数字人晋依依的第一视角&#xff0c;对山西的历史文化进行了回顾、展望与想象&#xff0c;利用数字人将山西原有的人文、环境、地貌等进一步宣传&#xff0c;带动当…

乐优商城(一)介绍和项目搭建

1. 乐优商城介绍 1.1 项目介绍 乐优商城是一个全品类的电商购物网站&#xff08;B2C&#xff09;用户可以在线购买商品、加入购物车、下单可以评论已购买商品管理员可以在后台管理商品的上下架、促销活动管理员可以监控商品销售状况客服可以在后台处理退款操作希望未来 3 到 …

【机器学习】集成学习(以随机森林为例)

文章目录 集成学习随机森林随机森林回归填补缺失值实例&#xff1a;随机森林在乳腺癌数据上的调参附录参数 集成学习 集成学习&#xff08;ensemble learning&#xff09;是时下非常流行的机器学习算法&#xff0c;它本身不是一个单独的机器学习算法&#xff0c;而是通过在数据…

Rust入门基础

文章目录 Rust相关介绍为什么要用Rust&#xff1f;Rust的用户和案例 开发环境准备安装Rust更新与卸载Rust开发工具 Hello World程序编写Rust程序编译与运行Rust程序 Cargo工具Cargo创建项目Cargo构建项目Cargo构建并运行项目Cargo检查项目Cargo为发布构建项目 Rust相关介绍 为…

关于串口服务器及转接线的一些基础知识笔记

1.普通个人计算机9针串口为232接口&#xff0c;部分特殊工业计算机为485接口。接线方式差异较大&#xff0c;容易区分。 2.串口服务器的作用&#xff1a;带串口的设备&#xff08;支持常见232、485/422接口方式&#xff09;&#xff0c;将其串口数据信号通过串口服务器转为网络…

角谷猜想:键盘输入一个整数,输出角谷猜想验证过程

键盘输入一个整数&#xff0c;输出角谷猜想验证过程。 (本笔记适合python循环、if条件语句、字符串格式化输出的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&…