操作系统真象还原:进一步完善内核

news2024/11/17 13:55:07

第12章-进一步完善内核

12.1 Linux系统调用浅析

系统调用就是让用户进程申请操作系统的帮助,让操作系统帮其完成某项工作,也就是相当于用户进程调用了操作系统的功能,因此“系统调用”准确地来说应该被称为“操作系统功能调用”。

Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信号Linux 只占用一个中断向量号,即 0x80 ,处理器执行指令 int 0x80 时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用之前, Linux 在寄存器 eax中写入子功能号,例如系统调用 open 和 close 都是不同的子功能号,当用户程序通过int 0x80 进行系统调用时,对应的中断处理例程会根据eax的值来判断用户进程申请哪种系统调用。

12.2 系统调用实现

上节已经跟大伙儿介绍了 Linux 系统调用的部分实现,本节以它为范本,参照着实现自己的系统调用。

12.2.1 系统调用实现框架

一个系统功能调用分为两部分, 一部分是暴露给用户进程的接口函数,它属于用户空间,此部分只是用户进程使用系统调用的途径,只负责发需求。另一部分是与之对应的内核具体实现,它属于内核空间,此部分完成的是功能需求,就是我们一直所说的系统调用子功能处理函数。为区分这两部分,一般情况下内核空间的函数名要在用户空间函数名前加“ sys_"

梳理下咱们系统调用的实现思路:

  1. 用中断门实现系统调用,效仿 Linux 用 0x80 号中断作为系统调用的入口
  2. 在 IDT 中安装 0x80 号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程。
  3. 建立系统调用子功能表 syscall_table ,利用 eax寄存器中的子功能号在该表中索引相应的处理函数。
  4. 用宏实现用户空 间系统调用接口_syscall ,最大支持 3 个参数的系统调用,故只 需要完成_syscall[0-3]。寄存器传递参数, eax为子功能号, ebx保存第 1 个参数, ecx 保存第 2 个参数, edx 保存第 3 个参数 。

在这里插入图片描述

12.2.2 增加0x80号中断
//interrupt.c
#define IDT_DESC_CNT 0x81   //目前总共支持的中断数
......
extern uint32_t syscall_handler(void);
......
/*初始化中断描述符表*/
static void idt_desc_init(void){
    int i,last = IDT_DESC_CNT-1;
    for(i=0;i<IDT_DESC_CNT;i++){
        make_idt_desc(&idtr[i],IDT_DESC_ATTR_DPL0,inter_entry_table[i]);
    }
    /*单独处理系统调用,系统调用对应的中断门dpl为3,中断处理程序为单独的 syscall_handler */
    make_idt_desc(&idtr[last],IDT_DESC_ATTR_DPL3,syscall_handler);
    put_str(" idt_desc_init done\n");
}

12.2.3 实现系统调用
/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-24 09:37:00
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-24 09:41:22
 * @FilePath: /OS/chapter12/12.2/lib/user/syscall.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "syscall.h"

/*无参数的系统调用*/
#define _syscall0(NUMBER)   ({  \
    int retval; \
    asm volatile(   \
    "int $0x80"    \
    : "=a"(retval)  \
    : "a"(NUMBER)   \
    : "memory"  \
    );  \
    retval; \
})

/*一个参数的系统调用*/
#define _syscall1(NUMBER, ARG1)   ({  \
    int retval; \
    asm volatile(   \
    "int $0x80"    \
    : "=a"(retval)  \
    : "a"(NUMBER), "b"(ARG1)   \
    : "memory"  \
    );  \
    retval; \
})

/*两个参数的系统调用*/
#define _syscall2(NUMBER, ARG1, ARG2)   ({  \
    int retval; \
    asm volatile(   \
    "int $0x80"    \
    : "=a"(retval)  \
    : "a"(NUMBER), "b"(ARG1), "c"(ARG2)   \
    : "memory"  \
    );  \
    retval; \
})

/*三个参数的系统调用*/
#define _syscall3(NUMBER, ARG1, ARG2, ARG3)   ({  \
    int retval; \
    asm volatile(   \
    "int $0x80"    \
    : "=a"(retval)  \
    : "a"(NUMBER), "b"(ARG1), "c"(ARG2), "d"(ARG3)   \
    : "memory"  \
    );  \
    retval; \
})
12.2.4 增加0x80号中断处理例程
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0x80号中断;;;;;;;;;;;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table
section .text
global syscall_handler
syscall_handler:
;1. 保存上下文
    push 0  ;为了统一栈中的格式,就手动压入一个0

    push ds
    push es
    push fs
    push gs
    pushad  ;PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI最先入栈

    push 0x80   ;此位置压入0x80也是为了保持统一的栈格式

;2. 为系统调用子功能传入参数
    push edx    ;系统调用中第3个参数
    push ecx    ;系统调用中第2个参数
    push ebx    ;系统调用中第1个参数

;3. 调佣子功能处理函数
    call [syscall_table + eax*4]
    add esp, 12    ;跨域过上面三个参数

;4. 将call调用后的返回值存入当前内核栈的eax位置
    mov [esp+8*4], eax
    jmp intr_exit
12.2.5 初始化系统调用和实现sys_getpid
/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-24 10:15:41
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-24 10:24:05
 * @FilePath: /OS/chapter12/12.2/userprog/syscall-init.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"
#include "thread.h"
#include "syscall.h"

#define syscall_nr  32
typedef void* syscall;

syscall syscall_table[syscall_nr];

/*返回当前任务的pid*/
uint32_t sys_getpid(void){
    return running_thread().pid;
}

/*初始化系统调用*/
void syscall_init(void){
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    put_str("syscall_init done\n");
}

我们先在进程/线程的pcb中添加pid成员

//thread.h
typedef int16_t pid_t;
......
struct task_struct{
    uint32_t* self_kstack;  //各内核线程都用自己的内核栈
    pid_t pid;
    enum task_status status;
......

pcb有了这个pid这个成员,那么自然我们创建进程/线程的时候要去为这个成员赋值

//thread.c
struct lock pid_lock;        //pid锁
.......
/*分配PID*/
static pid_t allocate_pid(void){
    static pid_t next_pid = 0;
    lock_acquire(&pid_lock);
    next_pid++;
    lock_release(&pid_lock);
    return next_pid;
}
......

/*初始化线程基本信息*/
void init_thread(struct task_struct* pthread, char* name, int prio){
    memset(pthread,0,sizeof(*pthread));
    pthread->pid = allocate_pid();
    strcpy(pthread->name,name);
......


/*初始化线程环境*/
void thread_init(){
    put_str("thread_init start\n");
    list_init(&thread_read_list);
    list_init(&thread_all_list);
    lock_init(&pid_lock);
    /*将当前main函数创建为线程*/
    make_main_thread();
    put_str("thread_init end\n");
}
12.2.6 添加系统调用getpid
//syscall.h
#ifndef __USER_SYSCALL_H
#define __USER_SYSCALL_H
#include "stdint.h"

enum SYSCALL_NR{
    SYS_GETPID
};

uint32_t getpid(void);
#endif // !__USER_SYSCALL_H

//syscall.c
......
/*返回当前任务的pid*/
uint32_t getpid(){
    return _syscall0(SYS_GETPID);
}

总结下增加系统调用的步骤:

  1. 在 syscall.h 中的结构 enum SYSCALL_NR 里添加新的子功能号。
  2. 在 syscall.c 中增加系统调用的用户接口。
  3. 在 syscall-init.c 中定义子功能处理函数井在 syscall_table 中注册 。
12.2.7 在用户进程中的系统调用
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int prog_a_pid = 0, prog_b_pid = 0;

int main(void) {
   put_str("I am kernel\n");
   init_all();

   process_execute(u_prog_a, "user_prog_a");
   process_execute(u_prog_b, "user_prog_b");

   intr_enable();
   console_put_str(" main_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
   thread_start("k_thread_a", 31, k_thread_a, "argA ");
   thread_start("k_thread_b", 31, k_thread_b, "argB ");
   while(1);
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {     
   char* para = arg;
   console_put_str(" thread_a_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
   console_put_str(" prog_a_pid:0x");
   console_put_int(prog_a_pid);
   console_put_char('\n');
   while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {     
   char* para = arg;
   console_put_str(" thread_b_pid:0x");
   console_put_int(sys_getpid());
   console_put_char('\n');
   console_put_str(" prog_b_pid:0x");
   console_put_int(prog_b_pid);
   console_put_char('\n');
   while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
   prog_a_pid = getpid();
   while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
   prog_b_pid = getpid();
   while(1);
}


一定要区分作为实际系统调用处理函数的sys_getpid与作为用户程序入口的getpid,前者是运行在内核态的,后者是用户态程序的入口去执行int 0x80的。

12.2.6 系统调用之栈传递参数
/* 无参数的系统调用 */
#define _syscall0(NUMBER) ({				       \
   int retval;					               \
   asm volatile (					       \
   "pushl %[number]; int $0x80; addl $4, %%esp"		       \
   : "=a" (retval)					       \
   : [number] "i" (NUMBER)		  		       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 一个参数的系统调用 */
#define _syscall1(NUMBER, ARG0) ({			       \
   int retval;					               \
   asm volatile (					       \
   "pushl %[arg0]; pushl %[number]; int $0x80; addl $8, %%esp" \
   : "=a" (retval)					       \
   : [number] "i" (NUMBER), [arg0] "g" (ARG0)		       \
   : "memory"						       \
   );							       \
   retval;						       \
})

/* 两个参数的系统调用 */
#define _syscall2(NUMBER, ARG0, ARG1) ({		       \
   int retval;						       \
   asm volatile (					       \
   "pushl %[arg1]; pushl %[arg0]; "			       \
   "pushl %[number]; int $0x80; addl $12, %%esp"	       \
      : "=a" (retval)					       \
      : [number] "i" (NUMBER),				       \
	[arg0] "g" (ARG0),				       \
	[arg1] "g" (ARG1)				       \
      : "memory"					       \
   );							       \
   retval;						       \
})

/* 三个参数的系统调用 */
#define _syscall3(NUMBER, ARG0, ARG1, ARG2) ({		       \
   int retval;						       \
   asm volatile (					       \
      "pushl %[arg2]; pushl %[arg1]; pushl %[arg0]; "	       \
      "pushl %[number]; int $0x80; addl $16, %%esp"	       \
      : "=a" (retval)					       \
      : [number] "i" (NUMBER),				       \
	[arg0] "g" (ARG0),				       \
	[arg1] "g" (ARG1),				       \
	[arg2] "g" (ARG2)				       \
      : "memory"					       \
   );							       \
   retval;						       \
})

;;;;;;;;;;;;;;;;   0x80号中断   ;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table            ;如同之前我们中断处理机制中引入了C中定义的中断处理程序入口地址表一样,这里引入了C中定义的系统调用函数入口地址表
section .text
global syscall_handler
syscall_handler:
                                ;1 保存上下文环境,为了复用之前写好的intr_exit:,所以我们仿照中断处理机制压入的东西,构建系统调用压入的东西
    push 0			            ; 压入0, 使栈中格式统一
    push ds
    push es
    push fs
    push gs
    pushad			            ; PUSHAD指令压入32位寄存器,其入栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI  
    push 0x80			        ; 此位置压入0x80也是为了保持统一的栈格式

                                ;2 从内核栈中获取cpu自动压入的用户栈指针esp的值
    mov ebx, [esp + 4 + 48 + 4 + 12]                             
                                
                                ; 为系统调用子功能传入参数,由于这个函数是3个参数的用户程序系统调用入口都会使用
                                ; 所以我们为了格式统一,直接按照最高参数数量压入3个参数,  此时ebx是用户栈指针
    push dword [ebx + 12]		; 系统调用中第3个参数
    push dword [ebx + 8]		; 系统调用中第2个参数
    push dword [ebx + 4]		; 系统调用中第1个参数
    mov edx, [ebx]              ; 系统调用的子功能号             

                                ;3 调用c中定义的功能处理函数
    call [syscall_table + edx*4]	; 编译器会在栈中根据C函数声明匹配正确数量的参数
    add esp, 12			        ; 跨过上面的三个参数

                                ;4 将call调用后的返回值存入待当前内核栈中eax的位置,c语言会自动把返回值放入eax中(c语言的ABI规定)
    mov [esp + 8*4], eax	
    jmp intr_exit		        ; intr_exit返回,恢复上下文


mov ebx, [esp + 4 + 48 + 4 + 12]
此时内核栈的栈顶位置 + 4 是跳过了压入的0x80,+ 48 是跳过 push ad 与 gs fs es ds,+ 4 是跳过了 push 0,最后 + 12是因为用户程序调用int 0x80触发软中断,然后导致特权级切换,cpu会自动向内核栈中按照顺序压入此时的用户程序执行时的ss, esp, eflag, cs, eip,跳过12字节(ss = 2, esp = 4, eflag = 4, cs = 2)就是此时用户栈的栈顶位置eip

12.3 让用户进程“说话”

12.3.1 可变参数的原理

操作系统只会在加载程序时为其分配内存,而且只分配这一次,我们把程序本身占用的内存称为静态内存 。我们把程序运行过程中额外需求的内存称为动态内存

随着计算机的进步,操作系统开始支持堆内存管理,堆内存专门用于程序运行时的内存申请,因此编译器也开始支持程序在运行时动态内存申请,也就是编译器开始支持源码中的变长数据结构。程序中的数据结构终归有个长度,此长度要么在编译时确定,要么在运行时确定。编译时确定是指数据结构在源码编译阶段就能确定下来,说白了就是编译器必须提前知道数据结构的长度,它为此类数据结构分配的是静态内存,也就是程序被操作系统加载时分配的内存 。

函数占用的也是静态内存,因此也得提前告诉编译器自己占用的内存大小,编译器要求提供函数声明 ,声明中描述了函数参数的个数及类型,编译器用它们来计算参数所占据的栈空间 。函数并不是在堆中分配内存,因此它需要提前确定内存空间,这通常取决于参数的个数及类型大小,但编译器却允许函数的参数个数不固定(可变参数)。

既然参数是由调用者压入(c调用约定)的,调用者当然知道战中压入了几个参数,参数占用了多少空间,因此无论函数的参数个数是否固定,采用 C 调用约定,调用者都能完好地回收技空间,不必担心栈溢出等问题 。

为方便引用函数中的可变参数,编译器 gcc 的头文件 stdarg.h 中定义了 3 个宏 ,是与可变参数相关的宏:这 3 个宏v_startva_endva_arg 都以 va开头,表示可变参数。

  1. v_start(ap,v):参数 ap 是用于指向可变参数的指针变量,参数 v 是支持可变参数的函数的第 1 个参数(如对于 printf 来说,参数 v 就是宇符串 format)此宏的功能是使指针 ap 指向 v 的地址,它的调用必须先于其他两个宏,相当于初始化 ap 指针的作用。
  2. va_arg(ap,t):参数 ap 是用于指向可变参数的指针变量,参数 t 是可变参数的类型此宏的功能是使指针 ap 指向战中下一个参数的地址并返回其值。
  3. va_end(ap):将指向可变参数的变量 ap 置为 null,也就是清空指针变量 ap
12.3.2 实现系统调用write

printf 函数是“格式化”“输出”函数,将格式化后的信息输出到标准输出(通常是屏幕)。但它只是个外壳,真正起到“格式化”作用的是 vsprintf 函数,真正起“输出”作用的是 write 系统调用。

//syscall-init.c
/*打印字符串str(未实现文件系统前的版本)*/
uint32_t sys_write(char* str){
   console_put_str(str);
   return strlen(len);
}

/* 初始化系统调用 */
void syscall_init(void) {
   put_str("syscall_init start\n");
   syscall_table[SYS_GETPID] = sys_getpid;
   syscall_table[SYS_WRITE] = sys_write;
   put_str("syscall_init done\n");
}

//syscall.c
/*打印字符串str*/
uint32_t write(char* str){
   return _syscall(SYS_WRITE,str);
}
12.3.3 实现printf

printf 是我们 C 语言标准输出函数,其原型是: int printf(const char *format,...);其中format就是格式化字符串,里面包含"%类型的字符串"

#include "stdio.h"
#include "interrupt.h"
#include "global.h"
#include "string.h"
#include "syscall.h"
#include "print.h"

#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4))	  // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL		  // 清除ap

/* 将整型转换成字符(integer to ascii) */
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
   uint32_t m = value % base;	    // 求模,最先掉下来的是最低位   
   uint32_t i = value / base;	    // 取整
   if (i) {			    // 如果倍数不为0则递归调用。
      itoa(i, buf_ptr_addr, base);
   }
   if (m < 10) {      // 如果余数是0~9
      *((*buf_ptr_addr)++) = m + '0';	  // 将数字0~9转换为字符'0'~'9'
   } else {	      // 否则余数是A~F
      *((*buf_ptr_addr)++) = m - 10 + 'A'; // 将数字A~F转换为字符'A'~'F'
   }
}

/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
   char* buf_ptr = str;
   const char* index_ptr = format;
   char index_char = *index_ptr;
   int32_t arg_int;
   char* arg_str;
   while(index_char) {
      if (index_char != '%') {
	 *(buf_ptr++) = index_char;
	 index_char = *(++index_ptr);
	 continue;
      }
      index_char = *(++index_ptr);	 // 得到%后面的字符
      switch(index_char) {        
        case 'x':
            arg_int = va_arg(ap, int);
            itoa(arg_int, &buf_ptr, 16); 
            index_char = *(++index_ptr); // 跳过格式字符并更新index_char
            break;
      }
   }
   return strlen(str);
}

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
   va_list args;
   va_start(args, format);	       // 使args指向format
   char buf[1024] = {0};	       // 用于存储拼接后的字符串
   vsprintf(buf, format, args);
   va_end(args);
   return write(buf); 
}

由于现在我们开启了页表机制,任何地址都将视作虚拟地址。我们之前编写print.S时,由于是给内核用的,所以用于与显存段打交道的地址有些是借助于内核页目录表0号项进行寻址的,现在我们将print共享给了用户进程,而用户进程无法去访问内核页目录表0号项。但是由于进程页目录表768号项与内核页目录表0号项指向同一张内核的页表(因为进程页目录表768号项就是拷贝的内核页目录表768号项)。所以我们能通过进程页目录表768号项访问原来通过内核页目录表0号项访问的地址。所以,我们需要修改print.S中的一些地址访问,将其升高3G,这样才能让原本通过内核页目录表0号项访问的地址,现在能通过进程页目录表768号项访问。

原文链接:https://blog.csdn.net/kanshanxd/article/details/131697078

.roll_screen:				                                ; 若超出屏幕大小,开始滚屏
    cld                                                     
    mov ecx, 960				                            ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
    ;mov esi, 0xb80a0			                            
	mov esi, 0xc00b80a0										; 第1行行首
    ;mov edi, 0xb8000			                            
	mov edi, 0xc00b8000										; 第0行行首
    rep movsd				                                ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw

修改Makefile为新增加的文件增加编译规则,编译之后,会出现如下错误:

ld: build/stdio.o: in function `printf':
stdio.c:(.text+0x1ab): undefined reference to `__stack_chk_fail'
make: *** [makefile:105: build/kernel.bin] Error 1

这个错误是因为编译器在使用栈保护特性,但链接器找不到实现这一特性所需的函数__stack_chk_fail。栈保护是一种安全特性,可以检测到栈溢出。通常情况下,__stack_chk_fail函数是由编译器自动插入到代码中的,用于在检测到栈溢出时中止程序。这个函数通常包含在C库中,由于我们链接的程序没有链接C库,就会看到这个错误。

解决这个问题的一个办法是禁用栈保护特性。在编译代码时添加-fno-stack-protector选项来做到这一点。

原文链接:https://blog.csdn.net/kanshanxd/article/details/131697078

12.3.4 进一步完善printf
#include "stdio.h"
#include "interrupt.h"
#include "global.h"
#include "string.h"
#include "syscall.h"
#include "print.h"

#define va_start(ap, v) ap = (va_list)&v  // 把ap指向第一个固定参数v
#define va_arg(ap, t) *((t*)(ap += 4))	  // ap指向下一个参数并返回其值
#define va_end(ap) ap = NULL		  // 清除ap

/* 将整型转换成字符(integer to ascii) */
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
   uint32_t m = value % base;	    // 求模,最先掉下来的是最低位   
   uint32_t i = value / base;	    // 取整
   if (i) {			    // 如果倍数不为0则递归调用。
      itoa(i, buf_ptr_addr, base);
   }
   if (m < 10) {      // 如果余数是0~9
      *((*buf_ptr_addr)++) = m + '0';	  // 将数字0~9转换为字符'0'~'9'
   } else {	      // 否则余数是A~F
      *((*buf_ptr_addr)++) = m - 10 + 'A'; // 将数字A~F转换为字符'A'~'F'
   }
}

/* 将参数ap按照格式format输出到字符串str,并返回替换后str长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
   char* buf_ptr = str;
   const char* index_ptr = format;
   char index_char = *index_ptr;
   int32_t arg_int;
   char* arg_str;
   while(index_char) {
      if (index_char != '%') {
	 *(buf_ptr++) = index_char;
	 index_char = *(++index_ptr);
	 continue;
      }
      index_char = *(++index_ptr);	 // 得到%后面的字符
      switch(index_char) {
        case 's':
            arg_str = va_arg(ap,char*);
            strcpy(buf_ptr,arg_str);
            buf_ptr+=strlen(arg_str);
            index_char = *(++index_ptr);
            break;
        case 'c':
            *(buf_ptr++) = va_arg(ap,char);
            index_char = *(++index_ptr);
            break;
        case 'd':
            arg_int = va_arg(ap,int);
            /*若是负数,将其转换为正数后,在正数前面输出个负号'-'*/
            if (arg_int<0)
            {
                arg_int = 0 - arg_int;
                *buf_ptr++='-';
            }
            itoa(arg_int,&buf_ptr,10);
            index_char = *(++index_ptr);
            break;
        case 'x':
            arg_int = va_arg(ap, int);
            itoa(arg_int, &buf_ptr, 16); 
            index_char = *(++index_ptr); // 跳过格式字符并更新index_char
            break;
      }
   }
   return strlen(str);
}


/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {
   va_list args;
   uint32_t retval;
   va_start(args, format);
   retval = vsprintf(buf, format, args);
   va_end(args);
   return retval;
}

/* 格式化输出字符串format */
uint32_t printf(const char* format, ...) {
   va_list args;
   va_start(args, format);	       // 使args指向format
   char buf[1024] = {0};	       // 用于存储拼接后的字符串
   vsprintf(buf, format, args);
   va_end(args);
   return write(buf); 
}

12.4 完善堆内存管理

12.4.1 malloc底层原理

之前我们虽然己经实现了内存管理,但显得过于粗糙,分配的内存都是以 4k大小的页框为单位的,当我们仅需要几十字节、几百字节这样的小内存块时,显然无法满足这样的需求了,为此必须实现一种小内存块的管理,可以满足任意内存大小的分配,这就是我们为实现 malloc 要做的基础工作。

arena 是很多开源项目中都会用到的内存管理概念,将一大块内存划分成多个小内存块,每个小内存块之间互不干涉,可以分别管理,这样众多的小内存块就称为arena。

arena 是个提供内在分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块数量,这其中包括内存块描述符指针(后面介绍),通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。另一部分就是内存地区域,这里面有无数的内存块,此部分占用 arena 大量的空间。我们把每个内存块命名为 mem_block,它们是内存分配粒度更细的资源,最终为用户分配的就是这其中的一个内存块。

在这里插入图片描述

当某一规arena中的内存块全部分配出去时,必须再增加新的同一规格的arena,由多个同一规格 的 arena 组合为一个“大的仓库”,为同一规格的内存块提供货源。 既然同一类内存块可以由多个 arena 提供,为了跟踪每个arena 中的空闲内存块,分别为每一种规格的内存块建立一个内存块描述符,即 mem_block_desc,在其中记录内存块规格大小,以及位于所有同类 arena 中的空闲内存块链表.
在这里插入图片描述

我们为 arena 分配 1 页框也就是 4KB 大小的内存,我们已经介绍过了,每个 arena 都分为两部分,一部分是占用空间很少的元信息,除元信息外的剩余部分才用于内存块的划分,因此,真正用于内存块的部分不足 4kb。内存块是平均划分的,所以最大的内存块肯定要小于 2k,这里我们以 2 为底的指数方程来划分内存块,因此最大的内存块是1024 字节,也就是说对内存块规格为 1024 字节的 arena 来说,它只有 3 个内存块,每个都是 1024 字节,剩余的部分就浪费了。**咱们这里的内存块以 16 字节为起始,向上依次是 32 宇节、 64 字节、 128 字节、256字节、 512 字节、 1024 字节,总共 7 种规格,因此,内存块描述符也就这 7 种。**再次强调一下,每种 arena中只有一种规格的内存块,并不是同时包含多种规格

总结,在内存管理系统中, arena 为任意大小内存的分配提供了统一的接口,它既支持 1024 字节以下的小块内存的分配,又支持大于 1024 字节以上的大块内存**, malloc 函数实际上就是通过 arena 申请这些内存块**。arena 是个内存仓库,并不直接对外提供内存分配,只有内存块描述符才对外提供内存块,内存块描述符将同类 arena 中的空闲内存块汇聚到一起,作为某一规格内存块的分配入口。因此,内存块描述符与 arena 是一对多的关系,每个 arena 都要与唯一的内存块描述符关联起来,多个同一规格的 arena 为同一规格的内存块描述符供应内存块,它们各自的元信息中用内存块描述符指针指向同一个内存块描述符。
在这里插入图片描述

12.4.2 底层初始化

修改(OS/kernel/memory.h

#include "list.h"

/* 内存块 */
struct mem_block {
   struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
   uint32_t block_size;		 // 内存块大小
   uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.
   struct list free_list;	 // 目前可用的mem_block链表
};

#define DESC_CNT 7	   // 内存块描述符个数

修改(OS/kernel/memory.c

/* 内存仓库arena元信息 */
struct arena {
   struct mem_block_desc* desc;	 // 此arena关联的mem_block_desc
   uint32_t cnt;
   bool large;		   /* large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量 */
};

struct mem_block_descstruct mem_blockstruct arena的关系:

struct mem_block_desc描述了不同类型的小块,比如:4KB页面划分成不同的小块,如256个16B小块、8个512B的小块。512B的小块对应一个mem_block_desc,而16B的小块对应另一个。block_size就是记录这个mem_block_desc用于描述哪种大小的小内存块,比如512或者16。blocks_per_arena用于记录一个页面拆分成了多少个小块,比如8个或者256个。free_list用于管理可以分配的小块,也就是用于将可以分配的小块形成链表。

struct mem_block其实本意是用来描述这个由4KB页面二次划分而成的固定小块,但是作者为了实现更通用的管理逻辑,所以这个结构体里面只包含了一个用于管理这个空闲小块的链表节点。

struct arena用于描述这个arenadesc用于指向这个管理这种arenamem_block_desc结构体,cnt的值意义取决于large的值,如果large = true,那么表示本arena占用的页框数目,否则表示本arena中还有多少空闲小内存块可用。需要注意的是,一个mem_block_desc对应的arena数量可不止一个,其实很好理解,当一个arena的小内存块分配完毕,我们就要再分配一个新的页充当arena然后划分成固定大小的小块。

修改(OS/kernel/memory.c

struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组

//初始化管理不同种类型arena的不同mem_block_desc
void block_desc_init(struct mem_block_desc* desc_array) {				   
   	uint16_t desc_idx, block_size = 16;
   	for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
      	desc_array[desc_idx].block_size = block_size;
      	desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;	  
      	list_init(&desc_array[desc_idx].free_list);
      	block_size *= 2;         // 更新为下一个规格内存块
   }
}

/* 内存管理部分初始化入口 */
void mem_init() {
   put_str("mem_init start\n");
   uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
   mem_pool_init(mem_bytes_total);	  // 初始化内存池
   block_desc_init(k_block_descs);
   put_str("mem_init done\n");
}

内核有了管理不同类型arena的mem_block_desck数组。我们说进程是独立分配资源的单位,进程拥有自己的独立虚拟地址空间,那么进程也应该拥有管理自己不同类型arenamem_block_desc数组。这样进程分配内存,就去进程自己的mem_block_desc数组中找对应的mem_block_desc,然后通过free_list找到空闲小块就行了。

修改(OS/thread/thread.h)为task_struct添加u_block_desc,这样每个task_struct都有了这个mem_block_desc数组

/*进程或线程的pcb,程序控制块*/
struct task_struct{
    uint32_t* self_kstack;  //各内核线程都用自己的内核栈
    pid_t pid;
    enum task_status status;
    char name[16];
    uint8_t priority;     //线程优先级
    uint8_t ticks;  //每次在处理器上执行的时间滴答数
    /*任务自上 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;    //进程自己的页表的虚拟地址
    struct virtual_addr userprog_vaddr; //用户进程的虚拟地址
    struct mem_block_desc u_block_desc[DESC_CNT];    //用户进程内存块描述符
    uint32_t stack_magic;   ///栈的边界标记,用于检测栈的溢出
};

但是,我们只初始化进程的mem_block_desc数组

修改(OS/userprog/process.c

/* 创建用户进程 */
void process_execute(void* filename, char* name) { 
   /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */
   struct task_struct* thread = get_kernel_pages(1);
   init_thread(thread, name, default_prio); 
   create_user_vaddr_bitmap(thread);
   thread_create(thread, start_process, filename);
   thread->pgdir = create_page_dir();
   block_desc_init(thread->u_block_desc);	//创建mem_block_desc数组

   enum intr_status old_status = intr_disable();
   ASSERT(!elem_find(&thread_read_list, &thread->general_tag));
   list_append(&thread_read_list, &thread->general_tag);

   ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
   list_append(&thread_all_list, &thread->all_list_tag);
   intr_set_status(old_status);
}
12.4.3 实现sys_malloc
#include "interrupt.h"

/* 返回arena中第idx个内存块的地址,原理是: 在 arena 指针指向的页框中,跳过元信息部分,即 struct arena 的大小,再用 idx 乘以该arena 中内存块大小,最终的地址便是arena 中第 idx 个内存块的首地址,最后将其转换成 mem_block 类型后返回 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
	return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}

/* 返回内存块b所在的arena地址 ,用于将 7 种规格的内存块转换为内存块所在的arena,由于此类内存块所在的 arena 占据 1 个完整的自然页框,所以arena 中的内存块都属于这1 页框之内*/
static struct arena* block2arena(struct mem_block* b) {
   	return (struct arena*)((uint32_t)b & 0xfffff000);
}

/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {
	enum pool_flags PF;
	struct pool* mem_pool;
	uint32_t pool_size;
	struct mem_block_desc* descs;	//用于存储mem_block_desc数组地址
	struct task_struct* cur_thread = running_thread();

	/* 判断用哪个内存池*/
	if (cur_thread->pgdir == NULL) {     // 若为内核线程
		PF = PF_KERNEL; 
		pool_size = kernel_pool.pool_size;
		mem_pool = &kernel_pool;
		descs = k_block_descs;
	} 
	else {				      // 用户进程pcb中的pgdir会在为其分配页表时创建
		PF = PF_USER;
		pool_size = user_pool.pool_size;
		mem_pool = &user_pool;
		descs = cur_thread->u_block_desc;
	}

	/* 若申请的内存不在内存池容量范围内则直接返回NULL */
	if (!(size > 0 && size < pool_size)) {
		return NULL;
	}
	struct arena* a;
	struct mem_block* b;	
	lock_acquire(&mem_pool->lock);

	/* 超过最大内存块1024, 就分配页框 */
	if (size > 1024) {
		uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);    // 向上取整需要的页框数
		a = malloc_page(PF, page_cnt);
		if (a != NULL) {
			memset(a, 0, page_cnt * PG_SIZE);	 // 将分配的内存清0  

			/* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */
			a->desc = NULL;
			a->cnt = page_cnt;
			a->large = true;
			lock_release(&mem_pool->lock);
            //arena中可被用户使用的部分是内存池部分,也就是要跨过arena前面的元信息部分,用“(a+l)”跨过 arena 元信息,也就是跨过一个 struct arena 的大小
			return (void*)(a + 1);		 // 跨过arena大小,把剩下的内存返回
		} 
		else { 
			lock_release(&mem_pool->lock);
			return NULL; 
		}
	} 
	else {    // 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配
		uint8_t desc_idx;
		
		/* 从内存块描述符中匹配合适的内存块规格 */
		for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {
			if (size <= descs[desc_idx].block_size) {  // 从小往大后,找到后退出
				break;
			}
		}

	/* 若mem_block_desc的free_list中已经没有可用的mem_block,
		* 就创建新的arena提供mem_block */
		if (list_empty(&descs[desc_idx].free_list)) {
			a = malloc_page(PF, 1);       // 分配1页框做为arena
			if (a == NULL) {
				lock_release(&mem_pool->lock);
				return NULL;
			}
			memset(a, 0, PG_SIZE);

			/* 对于分配的小块内存,将desc置为相应内存块描述符, 
			* cnt置为此arena可用的内存块数,large置为false */
			a->desc = &descs[desc_idx];
			a->large = false;
			a->cnt = descs[desc_idx].blocks_per_arena;
			uint32_t block_idx;

			enum intr_status old_status = intr_disable();

			/* 开始将arena拆分成内存块,并添加到内存块描述符的free_list中 */
			for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {
				b = arena2block(a, block_idx);
				ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));
				list_append(&a->desc->free_list, &b->free_elem);	
			}
			intr_set_status(old_status);
		}    

	/* 开始分配内存块 */
		b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));
		memset(b, 0, descs[desc_idx].block_size);

		a = block2arena(b);  // 获取内存块b所在的arena
		a->cnt--;		   // 将此arena中的空闲内存块数减1
		lock_release(&mem_pool->lock);
		return (void*)b;
	}
}

12.4.4 内存的释放

内存管理系统不仅能分配内存,还应该能回收内存,这是最基本的内存管理机制。 内存的使用情况都是通过位图来管理的,因此,无论内存的分配或释放,本质上都是在设置相关位图中的相应位,都是在读写位图 。 回收物理地址就是将物理内存池位图中的相应位清 0,无需将该 4kb物理页框逐宇节清 0。回收虚拟地址就是将虚拟内存池位图中的相应位清 0。分配则是相反的,也就是将位图中相应位置为1即可。

  1. 在虚拟地址池中分配虚拟地址,相关的函数是 vaddr_get
  2. 在物理内存池中分配物理地址,相关的函数是 palloc
  3. 在页表中完成虚拟地址到物理地址的映射,相关的函数是 page_table_add

释放内存是与分配内存相反的过程,咱们对照着设计一套释放内存的方法。

  1. 在物理地址池中释放物理页地址,相关的函数是 pfree,操作的位图同 palloc
  2. 在页表中去掉虚拟地址的映射,原理是将虚拟地址对应 pte 的 P 位置 0,相关的函数是 page_table_pte_remove
  3. 在虚拟地址池中释放虚拟地址,相关的函数是 vaddr_remove ,操作的位图同 vaddr_get

只要把 pte 中的P位置为0就可以了,该位表示 pte 指向的物理页框的数据是否已在该物理页框中, CPU 只要检测到P位为0,就会认为该 pte 无效,根本不会关心 pte 所指向的物理页框的地址是否属于可访问的物理内存的范围 。总之,只要 pte 的P位为0, CPU 就认为该虚拟地址未做映射,从而达到删除虚拟地址的目的 。

//将物理地址pg_phy_addr回收到物理内存池,实质就是清除物理内存池中位图的位
void pfree(uint32_t pg_phy_addr) {
	struct pool* mem_pool;
	uint32_t bit_idx = 0;
	if (pg_phy_addr >= user_pool.phy_addr_start) {     // 用户物理内存池
		mem_pool = &user_pool;
		bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;
	} 
	else {	  // 内核物理内存池
		mem_pool = &kernel_pool;
		bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;
	}
	bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);	 // 将位图中该位清0
}

/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {
   uint32_t* pte = pte_ptr(vaddr);
   *pte &= ~PG_P_1;	// 将页表项pte的P位置0
   asm volatile ("invlpg %0"::"m" (vaddr):"memory");    //更新tlb
}

//在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址,实质就是清楚虚拟内存池位图的位
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
	uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0;
	if (pf == PF_KERNEL) {  // 内核虚拟内存池
		bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
		while(cnt < pg_cnt) {
			bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
		}
	} 
	else {  // 用户虚拟内存池
		struct task_struct* cur_thread = running_thread();
		bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
		while(cnt < pg_cnt) {
			bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
		}
	}
}

/* 释放以虚拟地址vaddr为起始的cnt个物理页框 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {
	uint32_t pg_phy_addr;
	uint32_t vaddr = (int32_t)_vaddr, page_cnt = 0;
	ASSERT(pg_cnt >=1 && vaddr % PG_SIZE == 0); 
	pg_phy_addr = addr_v2p(vaddr);  // 获取虚拟地址vaddr对应的物理地址

	/* 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外 */
	ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);
	
	/* 判断pg_phy_addr属于用户物理内存池还是内核物理内存池 */
	if (pg_phy_addr >= user_pool.phy_addr_start) {   // 位于user_pool内存池
		vaddr -= PG_SIZE;
		while (page_cnt < pg_cnt) {
			vaddr += PG_SIZE;
			pg_phy_addr = addr_v2p(vaddr);

			/* 确保物理地址属于用户物理内存池 */
			ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);

			/* 先将对应的物理页框归还到内存池 */
			pfree(pg_phy_addr);

				/* 再从页表中清除此虚拟地址所在的页表项pte */
			page_table_pte_remove(vaddr);

			page_cnt++;
		}
	/* 清空虚拟地址的位图中的相应位 */
		vaddr_remove(pf, _vaddr, pg_cnt);

	} 
	else {	     // 位于kernel_pool内存池
		vaddr -= PG_SIZE;	      
		while (page_cnt < pg_cnt) {
			vaddr += PG_SIZE;
			pg_phy_addr = addr_v2p(vaddr);
			/* 确保待释放的物理内存只属于内核物理内存池 */
			ASSERT((pg_phy_addr % PG_SIZE) == 0 && \
				pg_phy_addr >= kernel_pool.phy_addr_start && \
				pg_phy_addr < user_pool.phy_addr_start);
			
			/* 先将对应的物理页框归还到内存池 */
			pfree(pg_phy_addr);

				/* 再从页表中清除此虚拟地址所在的页表项pte */
			page_table_pte_remove(vaddr);

			page_cnt++;
		}
	/* 清空虚拟地址的位图中的相应位 */
		vaddr_remove(pf, _vaddr, pg_cnt);
	}
}

12.4.5 实现sys_free

现在,我们实现与arena模型分配机制对应的回收机制,并将之前的页回收封装进入,直接实现统一的内存回收系统调用sys_free

/*回收内存ptr*/
void sys_free(void* ptr){
    ASSERT(ptr!=NULL);
    if(ptr!=NULL){
        enum pool_flags PF;
        struct pool* mem_pool;

        /*判断是线程还是进程*/
        if(running_thread()->pgdir == NULL){
            ASSERT((uint32_t)ptr >= K_HEAP_START);
            PF = PF_KERNEL;
            mem_pool = &kernel_pool;
        }else{
            PF = PF_USER;
            mem_pool = &user_pool;
        }

        lock_acquire(&mem_pool->lock);
        struct mem_block* b = ptr;
        struct arena* a = block2arena(b);
        //把mem_block转换为arena,获取元信息
        ASSERT(a->large == 0 || a->large == 1);
        if(a->desc == NULL && a->large == true){    //大于1024的内存
            mfree_page(PF,a,a->cnt);
        }else{      //小于1024的内存
            /*先将内存块回收到free_list*/
            list_append(&a->desc->free_list, &b->free_elem);

            /*在判断此arena中的内存块是否都是空闲,如果是就释放arena*/
            if(++a.cnt == a->desc->blocks_per_arena){
                uint32_t block_idx;
                for(block_idx=0;block_idx<a->desc->blocks_per_arena;block_idx++){
                    struct mem_block* b = arena2block(a,block_idx);
                    ASSERT(elem_find(&a->desc->free_list,&b->free_elem));
                    list_remove(&b->free_elem);
                }
                mfree_page(PF,a,1);
            }
        }
        lock_release(&mem_pool->lock);
    }
}
12.4.6 实现系统调用malloc和free

修改/userprog/syscall-init.c

/* 初始化系统调用 */
void syscall_init(void) {
   put_str("syscall_init start\n");
   syscall_table[SYS_GETPID] = sys_getpid;
   syscall_table[SYS_WRITE] = sys_write;
   syscall_table[SYS_MALLOC] = sys_malloc;
   syscall_table[SYS_FREE] = sys_free;
   put_str("syscall_init done\n");
}

修改/userprog/syscall.c

/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {
   return (void*)_syscall1(SYS_MALLOC, size);
}

/* 释放ptr指向的内存 */
void free(void* ptr) {
   _syscall1(SYS_FREE, ptr);
}

修改/userprog/syscall.h

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-24 11:09:17
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-28 13:56:42
 * @FilePath: /OS/chapter12/12.3/lib/user/syscall.h
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H
#include "stdint.h"
enum SYSCALL_NR {
   SYS_GETPID,
   SYS_WRITE,
   SYS_MALLOC,
   SYS_FREE
};
uint32_t getpid(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);
#endif

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

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

相关文章

用Vue3和Plotly.js打造交互式3D图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 基于 Plotly.js 的动态图表交互&#xff1a;颜色和可见性控制 应用场景介绍 本代码旨在为数据可视化提供一个交互式图表&#xff0c;允许用户动态控制图表中线条的颜色和可见性。此功能对于探索大型数据集或突…

网络安全 文件上传漏洞-18 第十八关 Pass-18

点击进入第十八关&#xff0c;并选择显示代码&#xff1a; //index.php $is_upload false; $msg null; if (isset($_POST[submit])) {require_once("./myupload.php");$imgFileName time();$u new MyUpload($_FILES[upload_file][name], $_FILES[upload_file][tmp…

网络爬虫(一)

1. 深度优先爬虫&#xff1a;深度优先爬虫是一种以深度为优先的爬虫算法。它从一个起始点开始&#xff0c;先访问一个链接&#xff0c;然后再访问该链接下的链接&#xff0c;一直深入地访问直到无法再继续深入为止。然后回溯到上一个链接&#xff0c;再继续深入访问下一个未被访…

SAP 替代关系完全替代简介

最近用户在对长周期物料进行备料的时候又提出替代料的问题,主料库存不足的时候需要考虑替代料的在途库存,经常会忘了SAP标准的替代料逻辑,这次一次性把这个逻辑写清楚。 关于替代料的逻辑在前面的博文中测试多个替代料的使用场景 1、后继物料 2、组合替代 本文主要测试一下…

时序(流式)图谱数据仓库AbutionGraph功能介绍-Streaming Graph OLAM Database

AbutionGraph是一款端到端的流式数据实时分析的图谱数据库&#xff0c;实时&#xff08;流式写入实时、高QPS决策分析实时、流式预处理实时&#xff09;表现在&#xff1a; 构建实时查询QPS响应时长与历史数据量无关的图模型&#xff1b;接入流式数据并实时更新图计算指标&…

软考《信息系统运行管理员》-2.1信息系统运维的管理

2.1信息系统运维的管理 信息系统运维管理体系框架 信息系统运维管理主要流程的目标 标准化&#xff1a;通过流程框架&#xff0c;构件标准的运维流程流程化&#xff1a;将大部分运维工作流程化&#xff0c;确保工作可重复&#xff0c;并且这些工作都有质量的完成&#xff0c;…

设置Docker中时区不生效的问题

项目中使用docker-compose&#xff0c;并通过以下方式设置了时区 environment:- SET_CONTAINER_TIMEZONEtrue- CONTAINER_TIMEZONEAsia/Shanghai 但是并没有正确生效&#xff0c;网上有很多博客都在推荐这个做法&#xff0c;另外一种是使用标准环境标量 -TZAsia/Shangehai …

ARP 原理详解 二

只要确定了 IP 地址后&#xff0c;就能够向这个 IP 地址所在的主机发送数据报&#xff0c;这是我们所熟知的事情。 但是再往深了想&#xff0c;IP 地址只是标识网络层的地址&#xff0c;那么在网络层下方数据链路层是不是也有一个地址能够告诉对方主机自己的地址呢&#xff1f…

SAP实现特别总账的凭证预制

SAP实现特别总账的凭证预制 仔细理解只有”其他”的特殊总帐标识才可预制凭证这句话. F-29/f-48不可预制。F-29/f-48预制时出现错误消息号 FP 030&#xff0c;提示特殊总帐标志类型“汇票和”预付定金“的特别总帐标志的过帐代码不能预制&#xff0c;这是系统写死的&#xff…

[vue2/vue3] 详细剖析watch、computed、watchEffect的区别,原理解读

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是前端菜鸟的自我修养&#xff01;今天给大家分享【深入剖析watch、computed、watchEffect的区别】&#xff0c;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;原创不易&#xff0c;如果能帮助到带大家…

云卓SKYDROID-H30——科技改变未来

云卓H30采用高通处理器、搭载安卓嵌入式系统&#xff0c;拥有三个工作频率&#xff0c;让图像更清晰、延迟更低、距离远、抗干扰性强&#xff0c;支持多种接口&#xff0c;更有10.1寸高清工业级阳光可视屏&#xff0c;防尘耐磨&#xff0c;结构强度高&#xff0c;适用于各种严苛…

python自动化之schedule

目录 代码&#xff08;以每5秒1次为例&#xff09;: 每5分钟1次 每2小时1次 每天18:00执行 用到的库&#xff1a;schedule&#xff0c;time 实现的效果&#xff1a;按秒来运行任务&#xff0c;按分钟来运行任务&#xff0c;按小时来运行任务&#xff0c;按天来运行任务 代…

<电力行业> - 《第1课:电力行业的五大四小》

1 什么是电力行业的五大四小&#xff1f; 我们常说的电力行业的五大四小&#xff0c;指的是电力行业有实力的公司&#xff0c;分为&#xff1a;较强梯队的五大集团、较弱梯队的四小豪门。 五个实力雄厚的集团&#xff0c;分别是&#xff1a; 中国华能集团公司中国大唐集团公…

基于K线最短路径构造的非流动性因子

下载地址https://download.csdn.net/download/SuiZuoZhuLiu/89492221

如何实现电子签名签章功能?

随着技术的发展&#xff0c;传统的纸质合同签署方式逐渐暴露出效率低下、存储不便和安全性不足等问题。为了解决这些问题&#xff0c;电子签署服务为用户提供了一个安全、高效、环保的合同管理解决方案。 电子合同管理与签署平台的核心功能 1、用户管理&#xff1a;平台提供用…

【在线评论】不同视角下在线评论对客户满意度和推荐度的影响—推文分析—2024-07-01

今天的推文主题是【在线评论】&#xff0c;重点关注可以关注第四篇&#xff0c;很全面地分析了在线评论的信息多维性。 第一篇从客户的在线评论入手&#xff0c;将客户消费的动机为功利、享受、社会满足&#xff1b;第二篇是关于在线评论对消费者再次选择同一家酒店的机制探索…

强化学习原理入门-1绪论

1 绪论 1.1 这是一本什么书 强化学习算法&#xff1f; AlphaGo大胜世界围棋冠军李世石和柯洁事件&#xff0c;核心算法就用到了强化学习算法。 1.2 强化学习解决什么问题 案例1 非线性系统二级倒立摆 案例2 AlphaGo与柯洁的第二局棋 案例3 机器人学习站立 ...... 智能…

【程序大侠传】异步架构应用回调数据接收接口偶发NPE

前序 在这片浩瀚的代码江湖中&#xff0c;各大门派林立&#xff0c;各自修炼独门绝技&#xff0c;江湖中的侠士们分别担任着开发、测试、产品和运维的角色&#xff0c;共同守护着这片数字化的疆域。 开发门派&#xff1a;代码剑宗 代码剑宗的弟子们精通各种编程语言&#xff…

新款奔驰GLE350升级原厂空气悬挂系统有哪些功能

奔驰 GLE350 升级原厂空气悬挂带来了一系列显著的优势和功能&#xff1a; 1. 舒适性提升 • 能够根据不同的路况和驾驶模式自动调节悬挂硬度和高度&#xff0c;有效过滤路面颠簸&#xff0c;为驾乘者提供更加平稳、舒适的行驶体验。 2. 行驶高度调节 • 驾驶者可以手动或自…

网络攻防题录集

文章目录 第一章 网络攻防概述第二章 密码学第三章 网络协议脆弱性分析第四 自测题三第五章 自测题五第六章 自测题六第七章 自测题七第八章 自测题八第九章 自测题九第十章 自测题十第十一章 自测题十一第十二章 自测题十二第十三章 自测题十三 第一章 网络攻防概述 第一代安…