(最近有点事,做的比较慢。哦,不,抄的比较慢。。。)

Lab 3: User Environments

Introduction

在这个实验中,我们将实现操作系统的一些基本功能,来实现用户环境下的进程的正常运行。你将会加强JOS内核的功能,为它增添一些重要的数据结构,用来记录用户进程环境的一些信息;创建一个单一的用户环境,并且加载一个程序运行它。你也可以让JOS内核能够完成用户环境所作出的任何系统调用,以及处理用户环境产生的各种异常。

Getting Started

照着官网上做就行了。
然后会多出他说的那些文件,后面用到的时候再说。

Part A: User Environments and Exception Handling

让我们看看inc/env.h的文件,里面有用户环境的一些基本定义。我们直接分析分析一下。内核使用Env数据结构来跟踪每个用户环境。 在本实验中,最初只会创建一个环境,但您需要设计JOS内核以支持多个环境; lab4将通过允许用户环境fork其他环境来利用此功能。

env.h

/* See COPYRIGHT for copyright information. */#ifndef JOS_INC_ENV_H
#define JOS_INC_ENV_H#include <inc/types.h>
#include <inc/trap.h>
#include <inc/memlayout.h>typedef int32_t envid_t; //用户环境ID 变量,32位的。// An environment ID 'envid_t' has three parts:
//
// +1+---------------21-----------------+--------10--------+
// |0|          Uniqueifier             |   Environment    |
// | |                                  |      Index       |
// +------------------------------------+------------------+
//                                       \--- ENVX(eid) --/
//
// The environment index ENVX(eid) equals the environment's index in the
// 'envs[]' array.  The uniqueifier distinguishes environments that were
// created at different times, but share the same environment index.
// 这个ENV(eid) 可以获取在envs 数组里面的第几个。
// All real environments are greater than 0 (so the sign bit is zero).
// envid_ts less than 0 signify errors.  The envid_t == 0 is special, and
// stands for the current environment.  所有的 环境是大于0 的,envid_ts小于0是错误的 ,envid_t == 0 标示当前正在运行
//最大能支持同时活跃的进程数量
#define LOG2NENV		10
#define NENV			(1 << LOG2NENV)
#define ENVX(envid)		((envid) & (NENV - 1))// Values of env_status in struct Env
enum {ENV_FREE = 0,//空闲ENV_DYING,//僵尸进程ENV_RUNNABLE,//准备就绪ENV_RUNNING,//运行态ENV_NOT_RUNNABLE//阻塞状态
};// Special environment types 环境的特殊类型
enum EnvType {ENV_TYPE_USER = 0,
};// 环境结构体   就是一个PCB 对这个有兴趣的同志可以看看我 的剖析 linux1.0 源码,这个就是简化版的那个东西。
struct Env {struct Trapframe env_tf;	// Saved registers  储存寄存器,用于恢复状态struct Env *env_link;	    // Next free Env    下一个空闲结构体envid_t env_id;			    // Unique environment identifier 独立的标识符envid_t env_parent_id;	    // env_id of this env's parent   父亲标识符enum EnvType env_type;	    // Indicates special system environments 用于区别出来某特定的用户环境unsigned env_status;	   // Status of the environment  前面定义的那几个状态uint32_t env_runs;		  // Number of times environment has run 运行的次数// Address spacepde_t *env_pgdir;		// Kernel virtual address of page dir 这个变量存放着这个环境的页目录的虚拟地址
};
#endif // !JOS_INC_ENV_H

分析完后,就去看 kern/env.c
这个文件先不看完,就看看他定义了什么东西。

struct Env *envs = NULL;		// All environments 所有的环境
struct Env *curenv = NULL;		// The current env	当前环境
static struct Env *env_free_list;	// Free environment list 空闲环境列表

后面有一大堆介绍。Trapframe这个里面具体有啥,我们后面用到的时候再看。

Allocating the Environments Array

前两个 结构体,在kern/env.h 里面有进行扩展,现在练习让我们,为他分配一个空间并映射,就是像上次为kern_pages分配空间一样,并进行映射。

	//////////////////////////////////////////////////////////////////////// Make 'envs' point to an array of size 'NENV' of 'struct Env'.// LAB 3: Your code here.envs=(struct Env*)boot_alloc(NENV*sizeof(struct Env));memset(envs,0,NENV*sizeof(struct Env));

这个 和,上次实验是一样的,和分配kern_pgdir是一模一样的。

	//////////////////////////////////////////////////////////////////////// Map the 'envs' array read-only by the user at linear address UENVS// (ie. perm = PTE_U | PTE_P).// Permissions://    - the new image at UENVS  -- kernel R, user R//    - envs itself -- kernel RW, user NONE// LAB 3: Your code here.boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);

另外再复习一下上节课的内存分配
在这里插入图片描述


Creating and Running Environments

现在你需要去编写 kern/env.c 文件来运行一个用户环境了。由于你现在没有文件系统,所以必须把内核设置成能够加载内核中的静态二进制程序映像文件。
Lab3 里面的 GNUmakefile 文件在obj/user/目录下面生成了一系列的二进制映像文件。如果你看一下 kern/Makefrag 文件,你会发现一些奇妙的地方,这些地方把二进制文件直接链接到内核可执行文件中,只要这些文件是.o文件。其中在链接器命令行中的-b binary 选项会使这些文件被当做二进制执行文件链接到内核之后。
kern/ini.c中的i386_init(),你会看到代码运行的环境中,这些二进制图像之一。然而,关键的功能设置用户环境是不完整的;您需要填写他们进来。
我们照着他的意思去看看,发现相较于前几次实验,多了几行。

	// Lab 3 user environment initialization functionsenv_init();trap_init();#if defined(TEST)// Don't touch -- used by grading script!  这些不要碰,是从来测试的ENV_CREATE(TEST, ENV_TYPE_USER);  //env_create
#else// Touch all you want.ENV_CREATE(user_hello, ENV_TYPE_USER);
#endif // TEST*// We only have one user environment for now, so just run it.env_run(&envs[0]);

kern/env.h 里面可以看见这个宏的原型,就当他运行了几个不同的测试吧。我没找到这几个在哪。

#define ENV_PASTE3(x, y, z) x ## y ## z#define ENV_CREATE(x, type)						\do {								\extern uint8_t ENV_PASTE3(_binary_obj_, x, _start)[];	\env_create(ENV_PASTE3(_binary_obj_, x, _start),		\type);					\} while (0)

不出意外,我们的任务 就是补充多出来的这几个函数了。

  • env_init(): 初始化所有的在envs数组中的 Env结构体,并把它们加入到 env_free_list中。 还要调用 env_init_percpu,这个函数要配置段式内存管理系统,让它所管理的段,可能具有两种访问优先级其中的一种,一个是内核运行时的0优先级,以及用户运行时的3优先级。
  • env_setup_vm(): 为一个新的用户环境分配一个页目录表,并且初始化这个用户环境的地址空间中的和内核相关的部分。
  • region_alloc(): 为用户环境分配物理地址空间
  • load_icode(): 分析一个ELF文件,类似于boot loader做的那样,我们可以把它的内容加载到用户环境下。
  • env_create(): 利用env_alloc函数和load_icode函数,加载一个ELF文件到用户环境中
  • env_run(): 在用户模式下,开始运行一个用户环境。

现在开始,补充kern/env.c,

env_init()

// Mark all environments in 'envs' as free, set their env_ids to 0,
// and insert them into the env_free_list. 把所有env 加入 空闲列表,然后设置 id=0
// Make sure the environments are in the free list in the same order
// they are in the envs array (i.e., so that the first call to
// env_alloc() returns envs[0]). 就是顺序从 0 递增 
//
void
env_init(void)
{// Set up envs array// LAB 3: Your code here.//上面分析过 要从0 开始,所以我们倒着遍历。env_free_list=NULL;for	(size_t i=NENV-1;i>=0;i--){envs[i]->env_id=0;envs[i]->env_status=ENV_FREE;envs[i]->env_link=env_free_list;env_free_list=&envs[i];}// Per-CPU part of the initializationenv_init_percpu();
}

env_init() 中调用了env_init_percpu() 不知道这个是干啥的。根据注释,是初始化了GDT和段描述符。

// Load GDT and segment descriptors.
void
env_init_percpu(void)
{lgdt(&gdt_pd);// The kernel never uses GS or FS, so we leave those set to// the user data segment.asm volatile("movw %%ax,%%gs" : : "a" (GD_UD|3));asm volatile("movw %%ax,%%fs" : : "a" (GD_UD|3));// The kernel does use ES, DS, and SS.  We'll change between// the kernel and user data segments as needed.asm volatile("movw %%ax,%%es" : : "a" (GD_KD));asm volatile("movw %%ax,%%ds" : : "a" (GD_KD));asm volatile("movw %%ax,%%ss" : : "a" (GD_KD));// Load the kernel text segment into CS.asm volatile("ljmp %0,$1f\n 1:\n" : : "i" (GD_KT));// For good measure, clear the local descriptor table (LDT),// since we don't use it.lldt(0);
}

env_setup_vm()

初始化完 之后,因为trap()是下一个的暂时不用管,所以我们直接跳到create_env,创建这个第一个要干的肯定是分配内存,最开始要做的是分配一个页目录。这个页目录,肯定是要复制内核的一部分,因为内核那一部分,你是绝对不能动的。

//
// Initialize the kernel virtual memory layout for environment e. 初始化内核虚拟布局
// Allocate a page directory, set e->env_pgdir accordingly, 分配一个页目录给e->env_pgdir
// and initialize the kernel portion of the new environment's address space
// Do NOT (yet) map anything into the user portion
// of the environment's virtual address space.
//初始化内核部分,不用映射 用户部分。
// Returns 0 on success, < 0 on error.  Errors include:
//	-E_NO_MEM if page directory or table could not be allocated.
//成功返回 0 否则返回 -E_NO_MEM
static int
env_setup_vm(struct Env *e)
{int i;struct PageInfo *p = NULL;// Allocate a page for the page directory 分配了一个页目录if (!(p = page_alloc(ALLOC_ZERO)))return -E_NO_MEM;// Now, set e->env_pgdir and initialize the page directory.//现在设置 e->env_pgdir 然后初始化页面目录// Hint://    - The VA space of all envs is identical above UTOP//	(except at UVPT, which we've set below).va 所有 envs 的虚拟地址 都是相同的在UTOP上面//	See inc/memlayout.h for permissions and layout.//	Can you use kern_pgdir as a template?  Hint: Yes. 可以用kern_pgdir做一个模板//	(Make sure you got the permissions right in Lab 2.) //    - The initial VA below UTOP is empty. 初始化 虚拟地址在 UTOP 是空的//    - You do not need to make any more calls to page_alloc. 你不需要去做任何的page_alloc//    - Note: In general, pp_ref is not maintained for //	physical pages mapped only above UTOP, but env_pgdir //	is an exception -- you need to increment env_pgdir's//	pp_ref for env_free to work correctly.//    - The functions in kern/pmap.h are handy. // 自己翻译吧,只可意会不可言传// LAB 3: Your code here.p->pp_ref++;e->env_pgdir=(pde_t *)page2kva(p);memcpy(e->env_pgdir, kern_pgdir, PGSIZE);// UVPT maps the env's own page table read-only.  // Permissions: kernel R, user Re->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;return 0;
}

region_alloc

分配完页目录,然后就是要给用户创建空间。只有一个页目录,肯定是不行的,你必须要给用户程序使用的空间。

//
// Allocate len bytes of physical memory for environment env,
// and map it at virtual address va in the environment's address space.
// Does not zero or otherwise initialize the mapped pages in any way.
// Pages should be writable by user and kernel.
// Panic if any allocation attempt fails.
//分配len 字节的 物理空间给 用户环境env,映射他的虚拟地址在环境的地址空间,不要用任何方式初始化页面。权限是内核用户可写,出错就 panic
static void
region_alloc(struct Env *e, void *va, size_t len)
{// LAB 3: Your code here.// (But only if you need it for load_icode.)//// Hint: It is easier to use region_alloc if the caller can pass//   'va' and 'len' values that are not page-aligned.//   You should round va down, and round (va + len) up.//   (Watch out for corner-cases!)void *start=ROUNDDOWN(va,PGSIZE),*end=ROUNDUP(va+len,PGSIZE);for (void * addr=start;addr<end;addr+=PGSIZE){struct PageInfo* p=page_alloc(0);if(p==NULL){panic("region alloc failed: No more page to be allocated.\n");}else {if(page_insert(e->env_pgdir,p,addr, PTE_U | PTE_W)==-E_NO_MEM){panic("region alloc failed: page table couldn't be allocated.\n");}}}
}

写个函数之前,我们先去看看trap.h

load_icode

因为目前并没有文件系统,所以我们要需要分配的堆栈,并不是来自文件加载出来的。为了方便实验,JOS让我们像加载操作系统一样加载这些文件。这个里面用到了Trapframe,我去看了看这个东西,对于某个字段是干啥的完全没有注释所以我也不知道该分析。

//
// Set up the initial program binary, stack, and processor flags
// for a user process. 初始化进程的 二进制 栈 和 处理器
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
//这个程序只能调用在内核初始化,在运行第一个用户模式环境
// This function loads all loadable segments from the ELF binary image
// into the environment's user memory, starting at the appropriate
// virtual addresses indicated in the ELF program header. 加载所有的 可装载程序 从 ELF二进制映象文件到内存,开始在适当的虚拟地址在ELF 的头部
// At the same time it clears to zero any portions of these segments 段中任何部分初始化为0
// that are marked in the program header as being mapped
// but not actually present in the ELF file - i.e., the program's bss section.
//
// All this is very similar to what our boot loader does, except the boot
// loader also needs to read the code from disk.  Take a look at
// boot/main.c to get ideas.  很像boot loader 做的,可以参考
//
// Finally, this function maps one page for the program's initial stack.
//这个函数映射一个页为了初始化堆栈
// load_icode panics if it encounters problems.
//  - How might load_icode fail?  What might be wrong with the given input?
//
static void
load_icode(struct Env *e, uint8_t *binary)
{// Hints://  Load each program segment into virtual memory //  at the address specified in the ELF segment header.加载每个程序段到虚拟内存 在 具体的ELF 头文件//  You should only load segments with ph->p_type == ELF_PROG_LOAD. 只需要加载ph->p_type == ELF_PROG_LOAD//  Each segment's virtual address can be found in ph->p_va 每个段的虚拟地址可以在ph->p_va找到//  and its size in memory can be found in ph->p_memsz. 大小是 ph->p_memsz//  The ph->p_filesz bytes from the ELF binary, starting at 文件开始在binary + ph->p_offset,应该被复制到 虚拟地址 ph->p_va。//  'binary + ph->p_offset', should be copied to virtual address//  ph->p_va.  Any remaining memory bytes should be cleared to zero.其他剩下的空间初始化为0//  (The ELF header should have ph->p_filesz <= ph->p_memsz.) 头部文件应该 ph->p_filesz <= ph->p_memsz//  Use functions from the previous lab to allocate and map pages.//使用这个前面所写的函数//  All page protection bits should be user read/write for now. 所有页都是用户可读写的//  ELF segments are not necessarily page-aligned, but you can ELF 段可能不是页对齐。//  assume for this function that no two segments will touch//  the same virtual page.假设这个函数 不会两个段在同一个虚拟页////  You may find a function like region_alloc useful. 你可以发现 region_alloc是有用的////  Loading the segments is much simpler if you can move data//  directly into the virtual addresses stored in the ELF binary.//  So which page directory should be in force during//  this function? 如果你可以直接移动数据存到ELF 序列里面 架子段就很容易,所以 页目录应当使用在这个函数////  You must also do something with the program's entry point,//  to make sure that the environment starts executing there.//  What?  (See env_run() and env_pop_tf() below.)// 你必须对程序入口指针做点什么 确保 后面用的上。// LAB 3: Your code here.//根据,分析 首先需要做的一件事 应该是讲binary 转换成 ELF,参照bootmain。struct Proghdr *ph, *eph;struct Elf * ELF=(struct Elf *)binary;if (ELFHDR->e_magic != ELF_MAGIC)panic("The loaded file is not ELF format!\n");ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);eph = ph + ELFHDR->e_phnum;//装载 用户目录lcr3(PADDR(e->env_pgdir));//第二部应该是加载段到内存for(;ph<eph;ph++){//加载条件是  ph->p_type == ELF_PROG_LOAD,地址是 ph->p_va 大小ph->p_memszif(ph->p_type == ELF_PROG_LOAD){if (ph->p_filesz > ph->p_memsz)panic("load_icode failed: p_memsz < p_filesz.\n");region_alloc(e, ph->p_va,ph->p_memsz);//复制ph->p_filesz bytes ,其他的补0memset(ph->p_va,0,ph->p_memsz);memcpy(ph->p_va,binary + ph->p_offset,ph->p_filesz);}}lcr3(PADDR(kern_pgdir));//最后是入口地址  这个实在 inc/trap.h 里面定义的e->env_tf.tf_eip = ELFHDR->e_entry;// Now map one page for the program's initial stack// at virtual address USTACKTOP - PGSIZE.  这个函数刚写过// LAB 3: Your code here. gion_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
}

在写enc_creat之前,我们先来分析一下,我们并不需要写 env_alloc,这个函数你可以理解为初始化一个env。 我们不需要知道过分的细节,但是需要了解他做了什么。

env_alloc

//
// Allocates and initializes a new environment.
// On success, the new environment is stored in *newenv_store.
// 分配了一个新的 环境,成功 就存在了 *newenv_store
// Returns 0 on success, < 0 on failure.  Errors include: 失败返回两种
//	-E_NO_FREE_ENV if all NENV environments are allocated
//	-E_NO_MEM on memory exhaustion
//
int
env_alloc(struct Env **newenv_store, envid_t parent_id)
{int32_t generation;int r;struct Env *e;//首先判断空闲 环境if (!(e = env_free_list))return -E_NO_FREE_ENV;//设置页目录// Allocate and set up the page directory for this environment.if ((r = env_setup_vm(e)) < 0)return r;// Generate an env_id for this environment. 设置  env_id generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);if (generation <= 0)	// Don't create a negative env_id.generation = 1 << ENVGENSHIFT;e->env_id = generation | (e - envs);// Set the basic status variables. 设置基础信息e->env_parent_id = parent_id;e->env_type = ENV_TYPE_USER;e->env_status = ENV_RUNNABLE;e->env_runs = 0;// Clear out all the saved register state,// to prevent the register values// of a prior environment inhabiting this Env structure// from "leaking" into our new environment. 清空寄存器状态memset(&e->env_tf, 0, sizeof(e->env_tf));// Set up appropriate initial values for the segment registers.// GD_UD is the user data segment selector in the GDT, and// GD_UT is the user text segment selector (see inc/memlayout.h).// The low 2 bits of each segment register contains the// Requestor Privilege Level (RPL); 3 means user mode.  When// we switch privilege levels, the hardware does various// checks involving the RPL and the Descriptor Privilege Level// (DPL) stored in the descriptors themselves. 设置初始值e->env_tf.tf_ds = GD_UD | 3;e->env_tf.tf_es = GD_UD | 3;e->env_tf.tf_ss = GD_UD | 3;e->env_tf.tf_esp = USTACKTOP;e->env_tf.tf_cs = GD_UT | 3;// You will set e->env_tf.tf_eip later.  这个很眼熟吧,就是上个函数用的,这个就是入口地址// commit the allocation 空闲环境 指向另一个。env_free_list = e->env_link;*newenv_store = e;cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);return 0;
}

env_create

函数作用就是根据binary 创建一个env

//
// Allocates a new env with env_alloc, loads the named elf
// binary into it with load_icode, and sets its env_type.
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
// The new env's parent ID is set to 0.
// 分配一个新的env 通过env_alloc 加载elf,设置他的its env_type 这个函数只在内核初始化抵用,在跑第一个用户环境,父亲设置为  0
void
env_create(uint8_t *binary, enum EnvType type)
{// LAB 3: Your code here.struct Env * e;int r=env_alloc(&e,0);if(r!=0){cprintf("%e\n",r);panic("env_create:error");}load_icode(e,binary);e->env_type=type;
}

env_run

这个就是真正的用户环境运行了。

/
// Context switch from curenv to env e. 上下文切换到 e
// Note: if this is the first call to env_run, curenv is NULL.
//如果第一个调用 curenv 是空的
// This function does not return.
//
void
env_run(struct Env *e)
{// Step 1: If this is a context switch (a new environment is running): 如果有上下文切换//	   1. Set the current environment (if any) back to 第一步当前环境 就绪状态//	      ENV_RUNNABLE if it is ENV_RUNNING (think about//	      what other states it can be in),//	   2. Set 'curenv' to the new environment, 当前运行变成 新的环境//	   3. Set its status to ENV_RUNNING,  设置他的状态为 运行//	   4. Update its 'env_runs' counter, 更新计数//	   5. Use lcr3() to switch to its address space. 修改地址空间// Step 2: Use env_pop_tf() to restore the environment's 第二部 使用那个啥恢复环境//	   registers and drop into user mode in the//	   environment.// Hint: This function loads the new environment's state from 这个函数重新加载 新的用户转台 从啥//	e->env_tf.  Go back through the code you wrote above//	and make sure you have set the relevant parts of//	e->env_tf to sensible values. 确保 那个哈是个真确的值// LAB 3: Your code here.if(curenv!=NULL&&curenv->env_status==ENV_RUNNING){curenv->env_status=ENV_RUNNABLE;}curenv=e;// if(&curenv->env_tf==NULL)cprintf("***");e->env_status=ENV_RUNNING;e->env_runs++;lcr3(PADDR(curenv->env_pgdir));cprintf("%x\n",curenv->env_tf.tf_eip);env_pop_tf(&curenv->env_tf);panic("env_run not yet implemented");//这个注释不注释没啥影响,因为我们现在就运行了一个 env,上面那个函数已经转移了,等他再来运行这一行,说明整个操作系统已经结束了。
}

我们再分析分析这个文件里面一些其他的函数。


//
// Frees env e and all memory it uses.
//
void
env_free(struct Env *e)
{pte_t *pt;uint32_t pdeno, pteno;physaddr_t pa;// If freeing the current environment, switch to kern_pgdir// before freeing the page directory, just in case the page// gets reused.if (e == curenv)lcr3(PADDR(kern_pgdir));  //切换到内核// Note the environment's demise.cprintf("[%08x] free env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
//打印信息// Flush all mapped pages in the user portion of the address spacestatic_assert(UTOP % PTSIZE == 0); //刷新所有映射for (pdeno = 0; pdeno < PDX(UTOP); pdeno++) {// only look at mapped page tablesif (!(e->env_pgdir[pdeno] & PTE_P))continue;// find the pa and va of the page tablepa = PTE_ADDR(e->env_pgdir[pdeno]);pt = (pte_t*) KADDR(pa);// unmap all PTEs in this page table 取消所有映射for (pteno = 0; pteno <= PTX(~0); pteno++) {if (pt[pteno] & PTE_P)page_remove(e->env_pgdir, PGADDR(pdeno, pteno, 0));}// free the page table itselfe->env_pgdir[pdeno] = 0;page_decref(pa2page(pa));}// free the page directory 把页目录删掉pa = PADDR(e->env_pgdir);e->env_pgdir = 0;page_decref(pa2page(pa));// return the environment to the free liste->env_status = ENV_FREE;e->env_link = env_free_list;env_free_list = e;
}//
// Frees environment e.
//
void
env_destroy(struct Env *e)
{env_free(e);cprintf("Destroyed the only environment - nothing more to do!\n");while (1)monitor(NULL);
}//
// Restores the register values in the Trapframe with the 'iret' instruction.
// This exits the kernel and starts executing some environment's code.
//
// This function does not return. 
//
void
env_pop_tf(struct Trapframe *tf) //这个就是跳转,
{asm volatile("\tmovl %0,%%esp\n""\tpopal\n""\tpopl %%es\n""\tpopl %%ds\n""\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */"\tiret\n": : "g" (tf) : "memory");panic("iret failed");  /* mostly to placate the compiler */
}

一旦你完成上述子函数的代码,并且在QEMU下编译运行,系统会进入用户空间,并且开始执行hello程序,直到它做出一个系统调用指令int。但是这个系统调用指令不能成功运行,因为到目前为止,JOS还没有设置相关硬件来实现从用户态向内核态的转换功能。当CPU发现,它没有被设置成能够处理这种系统调用中断时,它会触发一个保护异常,然后发现这个保护异常也无法处理,从而又产生一个错误异常,然后又发现仍旧无法解决问题,所以最后放弃,我们把这个叫做"triple fault"。通常来说,接下来CPU会复位,系统会重启。

所以我们马上要来解决这个问题,不过解决之前我们可以使用调试器来检查一下程序要进入用户模式时做了什么。使用make qemu-gdb 并且在 env_pop_tf 处设置断点,这条指令应该是即将进入用户模式之前的最后一条指令。然后进行单步调试,处理会在执行完iret 指令后进入用户模式。然后依旧可以看到进入用户态后执行的第一条指令了,该指令是一个cmp指令,开始于文件 lib/entry.S 中。 现在使用 b *0x... 设置一个断点在hello文件(obj/user/hello.asm)中的sys_cputs函数中的 int $0x30 指令处。这个int指令是一个系统调用,用来展示一个字符到控制台。如果你的程序运行不到这个int指令,说明有错误。
其实不用上面那么麻烦,直接运行make qemu-gdb 然后输入c指令,最终make gdb 会停在 int $0x30,然后qemu 会显示错误"triple fault"。

(后面大部分都是理论文字,大部分都是翻译过来的,所以直接照搬了大佬门博客里面的。英语水平不好,怕翻译了看不懂)

Handling Interrupts and Exceptions

到目前为止,当程序运行到第一个系统调用int $0x30 时,就会进入错误的状态,因为现在系统无法从用户态切换到内核态。所以你需要实现一个基本的异常/系统调用处理机制,使得内核可以从用户态转换为内核态。你应该先熟悉一下X86的异常中断机制。

Basics of Protected Control Transfer

 异常(Exception)和中断(Interrupts)都是“受到保护的控制转移方法”,都会使处理器从用户态转移为内核态。在Intel的术语中,一个中断指的是由外部异步事件引起的处理器控制权转移,比如外部IO设备发送来的中断信号。一个异常则是由于当前正在运行的指令所带来的同步的处理器控制权的转移,比如除零溢出异常。

 为了能够确保这些控制的转移能够真正被保护起来,处理器的中断/异常机制通常被设计为:用户态的代码无权选择内核中的代码从哪里开始执行。处理器可以确保只有在某些条件下,才能进入内核态。在X86上,有两种机制配合工作来提供这种保护:

  1. 中断向量表:处理器保证中断和异常只能够引起内核进入到一些特定的,被事先定义好的程序入口点,而不是由触发中断的程序来决定中断程序入口点。
    X86允许多达256个不同的中断和异常,每一个都配备一个独一无二的中断向量。一个向量指的就是0到255中的一个数。一个中断向量的值是根据中断源来决定的:不同设备,错误条件,以及对内核的请求都会产生出不同的中断和中断向量的组合。CPU将使用这个向量作为这个中断在中断向量表中的索引,这个表是由内核设置的,放在内核空间中,和GDT很像。通过这个表中的任意一个表项,处理器可以知道:
    *需要加载到EIP寄存器中的值,这个值指向了处理这个中断的中断处理程序的位置。
    *需要加载到CS寄存器中的值,里面还包含了这个中断处理程序的运行特权级。(即这个程序是在用户态还是内核态下运行。)

  2. 任务状态段:处理器还需要一个地方来存放,当异常/中断发生时,处理器的状态,比如EIP和CS寄存器的值。这样的话,中断处理程序一会可以重新返回到原来的程序中。这段内存自然也要保护起来,不能被用户态的程序所篡改。
    正因为如此,当一个x86处理器要处理一个中断,异常并且使运行特权级从用户态转为内核态时,它也会把它的堆栈切换到内核空间中。一个叫做 “任务状态段(TSS)”的数据结构将会详细记录这个堆栈所在的段的段描述符和地址。处理器会把SSESPEFLAGSCSEIP以及一个可选错误码等等这些值压入到这个堆栈上。然后加载中断处理程序的CSEIP值,并且设置ESPSS寄存器指向新的堆栈。
    尽管TSS非常大,并且还有很多其他的功能,但是JOS仅仅使用它来定义处理器从用户态转向内核态所采用的内核堆栈,由于JOS中的内核态指的就是特权级0,所以处理器用TSS中的ESP0SS0字段来指明这个内核堆栈的位置,大小。

Types of Exceptions and Interrupts

 所有的由X86处理器内部产生的异常的向量值是031之间的整数。比如,页表错所对应的向量值是14.而大于31号的中断向量对应的是软件中断,由int指令生成;或者是外部中断,由外部设备生成。
在这一章,我们将扩展JOS的功能,使它能够处理0~31号内部异常。在下一章会让JOS能够处理48号软件中断,主要被用来做系统调用。在Lab 4中会继续扩展JOS使它能够处理外部硬件中断,比如时钟中断。

An Example

让我们试一下除0

  1. 处理器会首先切换自己的堆栈,切换到由TSSSS0ESP0字段所指定的内核堆栈区,这两个字段分别存放着GD_KDKSTACKTOP的值。
  2. 处理器把异常参数压入到内核堆栈中,起始于地址KSTACKTOP:在这里插入图片描述
  3. 因为我们要处理的是除零异常,它的中断向量是0,处理器会读取IDT表中的0号表项,并且把CS:EIP的值设置为0号中断处理函数的地址值。
  4. 中断处理函数开始执行处理中断。

对于某些特定类型的x86异常,除了上面图中要保存5五个字之外,还要再压入一个字,叫做错误码。比如页错误,就是其中一个实例。当压入错误码之后,内核堆栈的状态如下:
在这里插入图片描述

Nested Exceptions and Interrupts

处理器在用户态下和内核态下都可以处理异常或中断。只有当处理器从用户态切换到内核态时,才会自动地切换堆栈,并且把一些寄存器中的原来的值压入到堆栈上,并且调用IDT指定的合适的异常处理程序。但如果处理器已经由于正在处理中断而处在内核态下时(CS寄存器的低两位已经都是0),此时CPU只会向内核堆栈压入更多的值。通过这种方式,内核就可处理嵌套中断。

如果处理器已经在内核态下并且遇到嵌套中断,因为它不需要切换堆栈,所以它不需要存储原来的SSESP寄存器的值。如果这个异常类型不压入错误码,此时内核堆栈的就像下面这个样子:
在这里插入图片描述
这里有一个重要的警告,如果处理器在内核态下接受一个异常,而且由于一些原因,比如堆栈空间不足,不能把当前的状态信息(寄存器的值)压入到内核堆栈中时,那么处理器是无法恢复到原来的状态了,它会自动重启。

Setting Up the IDT

(又要准备干活了)

你现在应该有了建立IDT表以及JOS处理异常的基本信息。我们现在只需要开始建立表就行了。
是否记得lab 2里面的内存分布,最低的那一页就是存这个的。
然后我们去看看inc/trap.h,那个kern/trap.h自己看看就行了。
如果想知道各个中断具体是啥看这个。

trap.h

#ifndef JOS_INC_TRAP_H
#define JOS_INC_TRAP_H// Trap numbers
// These are processor defined:  这是各种中断  对于这些建议大家学学嵌入式,手写个CPU(我的github 上有个简单的...) 下面各种错误还是大家自行百度,我解释几个常用的
#define T_DIVIDE     0		// divide error 除0
#define T_DEBUG      1		// debug exception 
#define T_NMI        2		// non-maskable interrupt 非屏蔽中断???
#define T_BRKPT      3		// breakpoint	断点
#define T_OFLOW      4		// overflow		溢出
#define T_BOUND      5		// bounds check	边界检查?
#define T_ILLOP      6		// illegal opcode	非法操作码  
#define T_DEVICE     7		// device not available 	设备不可用
#define T_DBLFLT     8		// double fault 
/* #define T_COPROC  9 */	// reserved (not generated by recent processors)
#define T_TSS       10		// invalid task switch segment 无效任务段切换
#define T_SEGNP     11		// segment not present 段不存在
#define T_STACK     12		// stack exception 栈异常
#define T_GPFLT     13		// general protection fault
#define T_PGFLT     14		// page fault 页错误
/* #define T_RES    15 */	// reserved
#define T_FPERR     16		// floating point error  浮点错误
#define T_ALIGN     17		// aligment check 对齐检查
#define T_MCHK      18		// machine check  
#define T_SIMDERR   19		// SIMD floating point error// These are arbitrarily chosen, but with care not to overlap  下面可以任意选择,但是不要重叠
// processor defined exceptions or interrupt vectors. 应该就是 自定义 异常
#define T_SYSCALL   48		// system call
#define T_DEFAULT   500		// catchall#define IRQ_OFFSET	32	// IRQ 0 corresponds to int IRQ_OFFSET 	 外部中断// Hardware IRQ numbers. We receive these as (IRQ_OFFSET+IRQ_WHATEVER)
#define IRQ_TIMER        0
#define IRQ_KBD          1
#define IRQ_SERIAL       4
#define IRQ_SPURIOUS     7
#define IRQ_IDE         14
#define IRQ_ERROR       19#ifndef __ASSEMBLER__#include <inc/types.h>
//保存通用寄存器的值
struct PushRegs {/* registers as pushed by pusha */uint32_t reg_edi;uint32_t reg_esi;uint32_t reg_ebp;uint32_t reg_oesp;		/* Useless */uint32_t reg_ebx;uint32_t reg_edx;uint32_t reg_ecx;uint32_t reg_eax;
} __attribute__((packed));
//任务段
struct Trapframe {struct PushRegs tf_regs;uint16_t tf_es;uint16_t tf_padding1;uint16_t tf_ds;uint16_t tf_padding2;uint32_t tf_trapno;/* below here defined by x86 hardware   下面是x86 硬件定义的 */uint32_t tf_err;uintptr_t tf_eip;uint16_t tf_cs;uint16_t tf_padding3;uint32_t tf_eflags;/* below here only when crossing rings, such as from user to kernel 不知道是啥*/uintptr_t tf_esp;uint16_t tf_ss;uint16_t tf_padding4;
} __attribute__((packed));#endif /* !__ASSEMBLER__ */#endif /* !JOS_INC_TRAP_H */

最后你要实现的控制流的效果如下:
在这里插入图片描述每一个中断或异常都有相应定义在trapentry.S中中断处理程序,trap_init()将用这些中断处理程序的地址初始化IDT。每一个处理程序都应该在堆栈上构建一个结构体struct Trapframe,并且调用trap()函数指向这个结构体,trap()然后处理异常/中断,给他分配一个中断处理函数。

练习4 要你编辑上面说这些东西。我们跟着他走,TRAPHANDLER_NOECTRAPHANDLER_NOEC,我们看看是啥。

TRAPHANDLER_NOEC和TRAPHANDLER_NOEC

在这个文件里面,也就是为每个中断创建一个函数,然后调用trap()

###################################################################
# exceptions/interrupts
###################################################################/* TRAPHANDLER defines a globally-visible function for handling a trap. 定义了一个全局可见的函数,用来处理trap* It pushes a trap number onto the stack, then jumps to _alltraps.* Use TRAPHANDLER for traps where the CPU automatically pushes an error code.* 他会把 陷阱号自推入堆栈,然后跳转 _alltraps,使用这个可以自动推入 错误码。* You shouldn't call a TRAPHANDLER function from C, but you may* need to _declare_ one in C (for instance, to get a function pointer* during IDT setup).  You can declare the function with*   void NAME();  如果你想在C里面用要声明一下* where NAME is the argument passed to TRAPHANDLER.*//*  翻译过来 就是创建了一个 函数,name ,然后做了下面这些事*/
#define TRAPHANDLER(name, num)						\.globl name;		/* define global symbol for 'name' 第一全局符号name */	\.type name, @function;	/* symbol type is function  符号类型是函数*/		\.align 2;		/* align function definition 对齐函数定义 */		\name:			/* function starts here 函数定义 */		\pushl $(num);							\jmp _alltraps/* Use TRAPHANDLER_NOEC for traps where the CPU doesn't push an error code.* It pushes a 0 in place of the error code, so the trap frame has the same* format in either case.  这个 和上面的区别就是不会 压入  错误码,用0来替代了??*/
#define TRAPHANDLER_NOEC(name, num)					\.globl name;							\.type name, @function;						\.align 2;							\name:								\pushl $0;							\pushl $(num);							\jmp _alltraps

inc/trap.h已经分析过了。然后他说 我们需要实现_alltraps。还需要在trap_init() 里面实现初始化入口定义。然后SETGATE会帮助我们。所以我们去看看STEGATE干了啥.
由于我并不知道他在哪,所以我们用grep搜一下。发现在mmu.h里面,上次我们分析了一部分,因为后面的没有用上,我就注释了一部分。如果已经知道的了就直接跳过。

// Set up a normal interrupt/trap gate descriptor. 设置一个正常中断陷阱入口 描述符
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate. 1是trap 0是interrupt//   see section 9.6.1.3 of the i386 reference: "The difference between //看看那个啥//   an interrupt gate and a trap gate is in the effect on IF (the  //中断门和陷阱门有啥不一样在IF(中断允许的标志)上面//   interrupt-enable flag). An interrupt that vectors through an//中断向量通过 中断门重置 IF 从而组织其他中断中断当前中断。 //   interrupt gate resets IF, thereby preventing other interrupts from//   interfering with the current interrupt handler. A subsequent IRET// 然后然后用IRET 恢复。//   instruction restores IF to the value in the EFLAGS image on the//   stack. An interrupt through a trap gate does not change IF."//说的简单点,中断不能再次中断,trap 可以被中断。
// - sel: Code segment selector for interrupt/trap handler 代码段地址
// - off: Offset in code segment for interrupt/trap handler //代码段偏移
// - dpl: Descriptor Privilege Level - 特权等级
//	  the privilege level required for software to invoke //软件等级
//	  this interrupt/trap gate explicitly using an int instruction.//int 指令调用?
#define SETGATE(gate, istrap, sel, off, dpl)			\
{								\(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;		\(gate).gd_sel = (sel);					\(gate).gd_args = 0;					\(gate).gd_rsv1 = 0;					\(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;	\(gate).gd_s = 0;					\(gate).gd_dpl = (dpl);					\(gate).gd_p = 1;					\(gate).gd_off_31_16 = (uint32_t) (off) >> 16;		\
}// Set up a call gate descriptor.  //建立呼叫门描述??? 和上面好像没啥差距,就是少了个istrap
#define SETCALLGATE(gate, sel, off, dpl)           	        \
{								\(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;		\(gate).gd_sel = (sel);					\(gate).gd_args = 0;					\(gate).gd_rsv1 = 0;					\(gate).gd_type = STS_CG32;				\(gate).gd_s = 0;					\(gate).gd_dpl = (dpl);					\(gate).gd_p = 1;					\(gate).gd_off_31_16 = (uint32_t) (off) >> 16;		\
}

后面就是告诉你_alltraps 要实现啥。我们还是先实现第一个trapentry.S.

.text/** Lab 3: Your code here for generating entry points for the different traps.*//* 我现在也不知道为啥这个是这个  那个是那个*/
TRAPHANDLER_NOEC(t_divide, T_DIVIDE)
TRAPHANDLER_NOEC(t_debug, T_DEBUG)
TRAPHANDLER_NOEC(t_nmi, T_NMI)
TRAPHANDLER_NOEC(t_brkpt, T_BRKPT)
TRAPHANDLER_NOEC(t_oflow, T_OFLOW)
TRAPHANDLER_NOEC(t_bound, T_BOUND)
TRAPHANDLER_NOEC(t_illop, T_ILLOP)
TRAPHANDLER_NOEC(t_device, T_DEVICE)
TRAPHANDLER(t_dblflt, T_DBLFLT)
TRAPHANDLER(t_tss, T_TSS)
TRAPHANDLER(t_segnp, T_SEGNP)
TRAPHANDLER(t_stack, T_STACK)
TRAPHANDLER(t_gpflt, T_GPFLT)
TRAPHANDLER(t_pgflt, T_PGFLT)
TRAPHANDLER_NOEC(t_fperr, T_FPERR)
TRAPHANDLER(t_align, T_ALIGN)
TRAPHANDLER_NOEC(t_mchk, T_MCHK)
TRAPHANDLER_NOEC(t_simderr, T_SIMDERR)TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)/** Lab 3: Your code here for _alltraps*/_alltraps:pushl %dspushl %espushal /* push all general registers */movl $GD_KD, %eaxmovw %ax, %dsmovw %ax, %espush %espcall trap	

然后 trap_init()

void t_divide();
void t_debug();
void t_nmi();
void t_brkpt();
void t_oflow();
void t_bound();
void t_illop();
void t_device();
void t_dblflt();
void t_tss();
void t_segnp();
void t_stack();
void t_gpflt();
void t_pgflt();
void t_fperr();
void t_align();
void t_mchk();
void t_simderr();
void t_syscall();void
trap_init(void)
{extern struct Segdesc gdt[];// LAB 3: Your code here.SETGATE(idt[T_DIVIDE], 0, GD_KT, t_divide, 0);SETGATE(idt[T_DEBUG], 0, GD_KT, t_debug, 0);SETGATE(idt[T_NMI], 0, GD_KT, t_nmi, 0);SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3);SETGATE(idt[T_OFLOW], 0, GD_KT, t_oflow, 0);SETGATE(idt[T_BOUND], 0, GD_KT, t_bound, 0);SETGATE(idt[T_ILLOP], 0, GD_KT, t_illop, 0);SETGATE(idt[T_DEVICE], 0, GD_KT, t_device, 0);SETGATE(idt[T_DBLFLT], 0, GD_KT, t_dblflt, 0);SETGATE(idt[T_TSS], 0, GD_KT, t_tss, 0);SETGATE(idt[T_SEGNP], 0, GD_KT, t_segnp, 0);SETGATE(idt[T_STACK], 0, GD_KT, t_stack, 0);SETGATE(idt[T_GPFLT], 0, GD_KT, t_gpflt, 0);SETGATE(idt[T_PGFLT], 0, GD_KT, t_pgflt, 0);SETGATE(idt[T_FPERR], 0, GD_KT, t_fperr, 0);SETGATE(idt[T_ALIGN], 0, GD_KT, t_align, 0);SETGATE(idt[T_MCHK], 0, GD_KT, t_mchk, 0);SETGATE(idt[T_SIMDERR], 0, GD_KT, t_simderr, 0);SETGATE(idt[T_SYSCALL], 0, GD_KT, t_syscall, 3);// Per-CPU setup trap_init_percpu();
}

用这个可以过了,但是我看到一个非常骚的操作,也就是挑战.

#define TRAPHANDLER(name, num, ec, user)						\
.text;                                          \.globl name;		/* define global symbol for 'name' */	\.type name, @function;	/* symbol type is function */		\.align 2;		/* align function definition */		\name:			/* function starts here */		\.if ec==0;                              \pushl $0;                           \.endif;                                \pushl $(num);							\jmp _alltraps;                          \
.data;                                       \.long num, name, user
.data .globl trapEntrytrapEntry:
.text
TRAPHANDLER(trapEntry0, T_DIVIDE, 0, 0);
TRAPHANDLER(trapEntry1, T_DEBUG, 0, 0);
TRAPHANDLER(trapEntry2, T_NMI, 0, 0);
TRAPHANDLER(trapEntry3, T_BRKPT, 0, 3);
TRAPHANDLER(trapEntry4, T_OFLOW, 0, 0);
TRAPHANDLER(trapEntry5, T_BOUND, 0, 0);
TRAPHANDLER(trapEntry6, T_ILLOP, 0, 0);
TRAPHANDLER(trapEntry7, T_DEVICE, 0, 0);
TRAPHANDLER(trapEntry8, T_DBLFLT, 1, 0);
TRAPHANDLER(trapEntry10, T_TSS, 1, 0);
TRAPHANDLER(trapEntry11, T_SEGNP, 1, 0);
TRAPHANDLER(trapEntry12, T_STACK, 1, 0);
TRAPHANDLER(trapEntry13, T_GPFLT, 1, 0);
TRAPHANDLER(trapEntry14, T_PGFLT, 1, 0);
TRAPHANDLER(trapEntry16, T_FPERR, 0, 0);
TRAPHANDLER(trapEntry17, T_ALIGN, 1, 0);
TRAPHANDLER(trapEntry18, T_MCHK, 0, 0);
TRAPHANDLER(trapEntry19, T_SIMDERR, 0, 0);
//TRAPHANDLER(trapEntry20, T_SYSCALL, 1, 3);
.data.long 0, 0, 0
/** Lab 3: Your code here for _alltraps*/
.text
_alltraps:pushl %dspushl %espushal   /* push all general registers */movw $GD_KD, %axmovw %ax, %ds movw %ax, %espushl %espcall trapvoid
trap_init(void)
{extern struct Segdesc gdt[];extern long trapEntry[][3];// trapEntry[][0]: interrupt/exception vector// trapEntry[][1]: interrupt/exception handler trapEntry point// trapEntry[][2]: DPLfor (int i = 0; trapEntry[i][1] != 0; i++ )SETGATE(idt[trapEntry[i][0]], 0, GD_KT, trapEntry[i][1], trapEntry[i][2]);// Per-CPU setup trap_init_percpu();
}

神仙写法,看不懂,但是大致能理解啥意思。
骚不过,骚不过,真的骚不过。

Question

第一个没有必要回答了吧。不同中断处理不同。
第二个问题,好像问user/softint为啥会产生 trap 13 中断。
查看user/softint.c

// buggy program - causes an illegal software interrupt#include <inc/lib.h>void
umain(int argc, char **argv)
{asm volatile("int $14");	// page fault
}

调用int $14产生了一个软中断。当异常或中断是由int n,int 3,int 0指令产生时,处理器才会检查中断或陷阱门的DPL。此时CPL数值上必须小于或等于DPL。这个限制可以防止特权级为3的应用程序使用软件中断访问重要的异常处理过程。当用户级使用软件中断时会引发一个General Protection Exception,即trap 13

Part B: Page Faults, Breakpoints Exceptions, and System Calls

我们现在已经有了处理一部分中断的能力了,然我们来看看他做了啥。在中断最后一个函数_alltraps调用了trap(),然后我们去了kern/trap()里面。我们来分析分析。

void
trap(struct Trapframe *tf)
{// The environment may have set DF and some versions// of GCC rely on DF being clear CLD 清除DF 复位 干啥的也不知道asm volatile("cld" ::: "cc");// Check that interrupts are disabled.  If this assertion// fails, DO NOT be tempted to fix it by inserting a "cli" in// the interrupt path. 看中断有没有关了assert(!(read_eflags() & FL_IF));//检查EFLAGS寄存器的IF标志位是否置0,即忽略可屏蔽的外部中断cprintf("Incoming TRAP frame at %p\n", tf);if ((tf->tf_cs & 3) == 3) {//if语句判断TrapFrame中的cs寄存器的CPL是否等于3,即是否是从用户态触发的中断//如果从用户态触发的中断,检查当前进程是否存在,这个应该是检查monitor下是不能出现中断的,然后更新当前进程的env_tf域,并最终将tf指针更新为进程的env_tf域的指针,这么做的原因会在下一篇文章[启动用户进程,产生中断、系统调用的过程分析]中说明// Trapped from user mode.assert(curenv);// Copy trap frame (which is currently on the stack)// into 'curenv->env_tf', so that running the environment// will restart at the trap point.curenv->env_tf = *tf;// The trapframe on the stack should be ignored from here on.tf = &curenv->env_tf;}// Record that tf is the last real trapframe so// print_trapframe can print some additional information.//更新last_tflast_tf = tf;// Dispatch based on what type of trap occurred//于发生的中断的类型进行分发。trap_dispatch(tf);// Return to the current environment, which should be running.//回到进程的用户态assert(curenv && curenv->env_status == ENV_RUNNING);env_run(curenv);
}

也就是说 ,我们在 trap_dispatch()对中断进行了分配。

Handling Page Faults

缺页故障的中断向量为14(T_PGFLT)是一个很重要的异常,因为我们在后续的实验中,非常依赖于能够处理缺页中断的能力。当缺页中断发生时,系统会把引起中断的线性地址存放到控制寄存器CR2中。在trap.c中,已经提供了一个能够处理这种缺页异常的函数page_fault_handler()
所以我们就要分配到这个函数。这个if else 或者switch 判断一下就行,没啥说的不需要先做任何操作。

    switch(tf->tf_trapno) {case (T_PGFLT):page_fault_handler(tf);break; 

就这样就行了。接着我们去看看 page_fault_handler()

void
page_fault_handler(struct Trapframe *tf)
{uint32_t fault_va;// Read processor's CR2 register to find the faulting addressfault_va = rcr2();// Handle kernel-mode page faults.// LAB 3: Your code here.if(tf->tf_cs && 0x01 == 0) { //发生在内核态 就报错,因为如果是内核态出错,说明内核出问题了panic("page_fault in kernel mode, fault address %d\n", fault_va);}// We've already handled kernel-mode exceptions, so if we get here,// the page fault happened in user mode.// Destroy the environment that caused the fault.//如果是用户态,就删除这个进程cprintf("用户态内存出错 :[%08x] user fault va %08x ip %08x\n",curenv->env_id, fault_va, tf->tf_eip);print_trapframe(tf);env_destroy(curenv);
}

后面还会继续完善,当我们完成系统调用

The Breakpoint Exception

断点异常的中断向量为3(T_BRKPT),这个异常可以让调试器能够给程序加上断点。加断点的基本原理就是把要加断点的语句用一个1字节的INT 3软件中断指令替换,执行到INT 3时,会触发软中断。在JOS中,我们将通过把这个异常转换成一个伪系统调用,这样的话任何用户环境都可以使用这个伪系统调用来触发JOS kernel monitor。如果将JOS kernel monitor当做原始的调试器的话,断点异常的这种用法实际上是合理的。lib/panic.cpanic()函数的用户态实现就是在展示panic信息之后,调用int 3

这个我也每个搞懂,为啥是调用monitor.

        case (T_BRKPT):monitor(tf);        break;

后面的挑战,是要我们实现,单步调试啥的。我不会告辞。

Question

  1. 问你为啥运行breakpoint(怎么运行这个,前面有个练习是说了 run-name)可以是General Protection 也可以是是Breakpoint.这个是由trap_init 初始化的时候做的。和练习二是一样的问题。SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3);把最后这个3 换成0,你再跑一下就知道为啥了。DPL字段代表的含义是段描述符优先级(Descriptor Privileged Level),如果我们想要当前执行的程序能够跳转到这个描述符所指向的程序哪里继续执行的话,有个要求,就是要求当前运行程序的CPLRPL的最大值需要小于等于DPL,否则就会出现优先级低的代码试图去访问优先级高的代码的情况,就会触发general protection exception。那么我们的测试程序首先运行于用户态,它的CPL3,当异常发生时,它希望去执行 int 3指令,这是一个系统级别的指令,用户态命令的CPL一定大于 int 3DPL,所以就会触发general protection exception,但是如果把IDT这个表项的DPL设置为3时,就不会出现这样的现象了,这时如果再出现异常,肯定是因为我们还没有编写处理break point exception的程序所引起的,所以是break point exception。 简单来说,就是breakpoint假如设置在内核态,用户态就需要保护一下,进入内核态。
  2. 这个和上面差不多。

System calls

用户程序通过系统调用让内核帮它做事。当用户程序触发系统调用,处理器进入内核态。处理器和内核合作保存该用户程序当前的状态,然后由内核将执行相应的代码完成系统调用,最终回到用户程序继续执行。而用户程序到底是如何引起内核的注意,以及它如何说明它希望操作系统做什么事情的方法是有很多不同的实现方式的。

JOS内核中,我们会采用int指令触发一个处理器的中断。特别的,我们用int $0x30来代表系统调用中断。注意,中断0x30不是通过硬件产生的,应该允许用户代码能够产生0x30中断。

应用程序会把系统调用号以及系统调用的参数放到寄存器中。通过这种方法,内核就不需要去查询用户程序的堆栈或指令流了。系统调用号存放到%eax中,参数则存放在%edx,%ecx,%ebx,%edi, 和 %esi 中。内核会把返回值送到%eax中。在lib/syscall.c中的syscall()函数就是触发一个系统调用的代码。不用说了,我们先去看看。

// System call stubs.#include <inc/syscall.h>
#include <inc/lib.h>/* 来自一个大佬
* 在JOS中所有系统调用通过syscall这个函数进行:执行int T_SYSCALL,把函数参数存入若干指定的寄存器
* 并指定函数返回值返回到寄存器ax中
* 用第一个参数num来确定到底是哪个系统调用
* 参数num == SYS_cputs,check == 0,a1 == b->buf, a2 == b->idx,剩下a3、a4、a5都为0
*/
static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{int32_t ret;asm volatile("int %1\n"            //汇编指令模板,%1是占位符,对应后面的T_SYSCALL: "=a" (ret)          //=表示在汇编里只能改变该C变量的值,而不能取它的值//ret值与%ax相联系,即指令执行完后ax的值存入变量ret: "i" (T_SYSCALL),    //中断向量T_SYSCALL,是立即数"a" (num),          //输入参数num,指令执行前先将num变量的值存入%ax"d" (a1),           //输入参数a1,指令执行前先将a1变量的值存入%dx"c" (a2),           //参数a2存入%cx"b" (a3),           //参数a3存入%bx"D" (a4),           //参数a4存入%di"S" (a5),           //参数a5存入%si: "cc", "memory");    //向gcc声明在这条汇编语言执行后,标志寄存器eflags和内存可能发生改变//加入“memory”,告诉GCC内存已经被修改,GCC得知这个信息后, //就会在这段指令之前,插入必要的指令将前面因为优化缓存到寄存器中//的变量值先写回内存,如果以后又要使用这些变量再重新读取。if(check && ret > 0)panic("syscall %d returned %d (> 0)", num, ret);return ret;
}
//下面是各个函数。 
//输出?? 在控制台输入输出 是要进入内核态的
void
sys_cputs(const char *s, size_t len)
{syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}
//获取???
int
sys_cgetc(void)
{return syscall(SYS_cgetc, 0, 0, 0, 0, 0, 0);
}
//删除???
int
sys_env_destroy(envid_t envid)
{return syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0);
}
//获取id???
envid_t
sys_getenvid(void)
{return syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0);
}

看完之后来做练习7,那个啥,让我们去把这个加入异常,我们原本就已经加入进去了,不用管。后面我就看不懂了…,我发现系统内核里面还有个kern/syscall.c这个是干啥的。到底调用哪个。。。别人说kern/syscall.c是外壳,但是我个人感觉inc/syscall.c才是。我觉得应该是inc/syscall.c调用了kern/syscal.c不知道对不对,我单步调试,并查看hello.asm文件其中调用了sys_getenvid 。将断点打到0x800b15可以看见。
要在lib/libmain.c里面调用sys_getenvid。先不用管这个是啥,下个实验会讲,先把这个添进去调试。

void
libmain(int argc, char **argv)
{// set thisenv to point at our Env structure in envs[].// LAB 3: Your code here.thisenv = &envs[ENVX(sys_getenvid())];// save the name of the program so that panic() can use itif (argc > 0)binaryname = argv[0];// call user main routineumain(argc, argv);// exit gracefullyexit();
}
  800b15:	55                   	push   %ebp800b16:	89 e5                	mov    %esp,%ebp800b18:	57                   	push   %edi800b19:	56                   	push   %esi800b1a:	53                   	push   %ebx//// The last clause tells the assembler that this can// potentially change the condition codes and arbitrary// memory locations.asm volatile("int %1\n"800b1b:	ba 00 00 00 00       	mov    $0x0,%edx800b20:	b8 02 00 00 00       	mov    $0x2,%eax800b25:	89 d1                	mov    %edx,%ecx800b27:	89 d3                	mov    %edx,%ebx800b29:	89 d7                	mov    %edx,%edi800b2b:	89 d6                	mov    %edx,%esi800b2d:	cd 30                	int    $0x30

能够明显的看见调用额 int30,所以应该是 用户通过inc/syscall.c进行系统调用。
后面就比较简单了。
前面也已经提示你了,所以我们直接调用就可以了。

        case (T_SYSCALL):ret_code = syscall(tf->tf_regs.reg_eax,tf->tf_regs.reg_edx,tf->tf_regs.reg_ecx,tf->tf_regs.reg_ebx,tf->tf_regs.reg_edi,tf->tf_regs.reg_esi);tf->tf_regs.reg_eax = ret_code;

sysycall里面判断信号,分别调用哪几个函数。

// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{// Call the function corresponding to the 'syscallno' parameter.// Return any appropriate return value.// LAB 3: Your code here.//    panic("syscall not implemented");switch (syscallno) {case (SYS_cputs):sys_cputs((const char *)a1, a2);return 0;case (SYS_cgetc):return sys_cgetc();case (SYS_getenvid):return sys_getenvid();case (SYS_env_destroy):return sys_env_destroy(a1);default:return -E_INVAL;}
}

大家多用gdb 调试自己查看程序运行过程,这样可以理解更快。

挑战我就不看了,一般都是做不出来的,主要是没时间查看相关资料。

User-mode startup

上一个实验已经把代码给了,最后那一块如果好好理解了的话,这个基本上就能直接过了。
用户程序真正开始运行的地方是在lib/entry.S文件中。该文件中,首先会进行一些设置,然后就会调用lib/libmain.c 文件中的 libmain() 函数。你首先要修改一下 libmain() 函数,使它能够初始化全局指针 thisenv,让它指向当前用户环境的 Env 结构体。
然后 libmain() 函数就会调用 umain,这个 umain 程序恰好是 user/hello.c 中被调用的函数。在之前的实验中我们发现,hello.c程序只会打印 hello, world 这句话,然后就会报出 page fault 异常,原因就是 thisenv->env_id 这条语句。现在你已经正确初始化了这个 thisenv的值,再次运行就应该不会报错了。

不理解的可以继续单步调试。断点打在f0103003

Page faults and memory protection

这个练习,我们已经做了一点了,前那个函数分配page_fault_handler的时候我已经把page_fault_handler 完善了。这里就是告诉你 内核如果缺页,说明内核出问题了,不能继续运行了,必须报错panic。如果是用户能解决就解决,解决不了就删除。

void
page_fault_handler(struct Trapframe *tf)
{uint32_t fault_va;// Read processor's CR2 register to find the faulting addressfault_va = rcr2();// Handle kernel-mode page faults.// LAB 3: Your code here.if(tf->tf_cs && 0x01 == 0) {panic("page_fault in kernel mode, fault address %d\n", fault_va);}// We've already handled kernel-mode exceptions, so if we get here,// the page fault happened in user mode.// Destroy the environment that caused the fault.cprintf("[%08x] user fault va %08x ip %08x\n",curenv->env_id, fault_va, tf->tf_eip);print_trapframe(tf);env_destroy(curenv);
}

然后根据题目的要求,我们还要继续完善 kern/pmap.c 文件中的 user_mem_assert , user_mem_check 函数,通过观察 user_mem_assert 函数我们发现,它调用了 user_mem_check 函数。而 user_mem_check 函数的功能是检查一下当前用户态程序是否有对虚拟地址空间 [va, va+len] 的 perm| PTE_P 访问权限。
自然我们要做的事情应该是,先找到这个虚拟地址范围对应于当前用户态程序的页表中的页表项,然后再去看一下这个页表项中有关访问权限的字段,是否包含 perm | PTE_P,只要有一个页表项是不包含的,就代表程序对这个范围的虚拟地址没有 perm|PTE_P 的访问权限。以上就是这段代码的大致思想。

//这个函数分析 先挖个坑,做下个实验之前,来填一下。
//
// Check that an environment is allowed to access the range of memory
// [va, va+len) with permissions 'perm | PTE_P'. 检查内存权限是不是 这个
// Normally 'perm' will contain PTE_U at least, but this is not required.
// 'va' and 'len' need not be page-aligned; you must test every page that
// contains any of that range.  You will test either 'len/PGSIZE',
// 'len/PGSIZE + 1', or 'len/PGSIZE + 2' pages.
//
// A user program can access a virtual address if (1) the address is below
// ULIM, and (2) the page table gives it permission.  These are exactly
// the tests you should implement here.
// 地址 应该在ULIM之下 权限应该对
// If there is an error, set the 'user_mem_check_addr' variable to the first
// erroneous virtual address.
//
// Returns 0 if the user program can access this range of addresses,
// and -E_FAULT otherwise.
//
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{// LAB 3: Your code here.char * end = NULL;char * start = NULL;start = ROUNDDOWN((char *)va, PGSIZE); end = ROUNDUP((char *)(va + len), PGSIZE);pte_t *cur = NULL;for(; start < end; start += PGSIZE) {cur = pgdir_walk(env->env_pgdir, (void *)start, 0);if((int)start > ULIM || cur == NULL || ((uint32_t)(*cur) & perm) != perm) {if(start == ROUNDDOWN((char *)va, PGSIZE)) {user_mem_check_addr = (uintptr_t)va;}else {user_mem_check_addr = (uintptr_t)start;}return -E_FAULT;}}return 0;
}
// Print a string to the system console.
// The string is exactly 'len' characters long.
// Destroys the environment on memory errors.
static void
sys_cputs(const char *s, size_t len)
{// Check that the user has permission to read memory [s, s+len).// Destroy the environment if not:.// LAB 3: Your code here.user_mem_assert(curenv, s, len, 0);// Print the string supplied by the user.cprintf("%.*s", len, s);
}
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 以单链表为数据结构的一个学生信息输入读取文件程序(尾插法)

    #include<stdio.h> #include<stdlib.h> struct Node {char name[10];int num;int age;char addr[15];struct Node *next; }; struct Node *creat_inf();//输入数据到单链表 void save_inf(struct Node *h);//把单链表的信息写到文件 struct Node *read_inf();//从文…...

    2024/3/29 7:16:36
  2. 老戴里都是你

    星宫之君醉琼浆昊然情性雪晴天我念乾坤德泰大想得越人今夜见你如玉彻天生成了然炅炅双瞳子...

    2024/4/11 12:59:22
  3. 阿里大于技术_如何给手机发送验证码?

    如何给手机发送验证码? 一、前言 技术名称:阿里大于短信验证(发送短信验证的其中的一种方式,比较权威)。 短信的发布流程:我们通常不用自己写代码(因为我们通常没有资质,如果自己写的话代码量会很大),找第三方的服务给我们写代码。 调用第三方的服务(例如天气服务等…...

    2024/3/29 7:16:40
  4. C++Primer 拷贝控制和资源管理

    类型对象的拷贝语义,一般来说,有两者选择:可以定义拷贝操作,使类的行为看起来像一个值或者一个指针。 类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本也会改变原对象,反之亦然。 行为像指针的类则共享状…...

    2024/4/16 23:48:05
  5. Mybatis注解开发环境配置

    Mybatis注解开发环境配置 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http:/…...

    2024/3/29 7:16:32
  6. python//Jan.18th,2020//二分查找

    def binary_search(list,item):low=0high=len(list)-1while low<=high:mid=(low+high)//2guess=list[mid]if guess ==item:return midif guess>item:high=mid-1else:low=mid+1return None my_list=[1,3,5,7,9] s=binary_search(my_list,5) print(s)...

    2024/3/29 7:16:31
  7. c语言尺取算法-王祥力3班

    尺取法:尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。它就跟毛毛虫一样一步步往前取,但每次都会把两端点变小,之所以需要掌握这个技巧,是因为尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的 时候…...

    2024/4/1 11:45:18
  8. 蓝桥杯 历届试题 对局匹配(dp)

    问题描述小明喜欢在一个围棋网站上找别人在线对弈。这个网站上所有注册用户都有一个积分,代表他的围棋水平。小明发现网站的自动对局系统在匹配对手时,只会将积分差恰好是K的两名用户匹配在一起。如果两人分差小于或大于K,系统都不会将他们匹配。现在小明知道这个网站总共有…...

    2024/3/29 7:16:29
  9. 《Git Community Book》之GIT对象模型

    对象每个对象(object) 包括三个部分:类型,大小和内容。大小就是指内容的大小,内容取决于对象的类型,有四种类型的对象:"blob"、"tree"、 "commit" 和"tag"。“blob”用来存储文件数据,通常是一个文件。“tree”有点像一个目录,…...

    2024/4/7 0:01:12
  10. Python学习笔记-Python 字典(Dictionary)

    字典的每个键值 key=>value 对用冒号 : 分割,每个键值对之间用逗号 , 分割,整个字典包括在花括号 {} 中 ,格式如下所示:d = {key1 : value1, key2 : value2 }键一般是唯一的,如果重复最后的一个键值对会替换前面的,值不需要唯一。>>>dict = {a: 1, b: 2, b: 3…...

    2024/3/29 7:16:27
  11. keep-alive

    一 keep-alive是Vue的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。 <keep-alive><loading></loading> </keep-laive>二 当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行。 三 被包裹…...

    2024/3/29 7:16:10
  12. springcloud Config客户端连接注册中心开启重试

    1.为什么要开启 如果网络波动导致无法连接config注册中心的原因,导致启动直接失败,似乎不是很好。所以,config客户端提供了自动重试的功能。 2。开启之前做的准备 2.1 开启重试功能之前,先确保已经配置了spring.cloud.config.failFast=true。这样做的原因是:希望config客户…...

    2024/4/18 20:33:37
  13. POJ 2449 Remmarguts' Date(两点间K短路)

    题意: 给出n个点,m条单向边的图,最后一行输入s,t,k,询问s到t的第k短路题解: 由于这道题是单向边,所以我们需要建反图来求出h(i),h(i)为终点到i的最短路径,然后A*套一套就OK了AC代码: #pragma GCC optimize(2) //#include<bits/stdc++.h> #include<iostream…...

    2024/3/29 7:16:05
  14. A. Plate Game

    链接:https://codeforces.com/problemset/problem/197/AYouve got a rectangular table with length a and width b and the infinite number of plates of radius r. Two players play the following game: they take turns to put the plates on the table so that the plat…...

    2024/3/29 7:16:04
  15. 日常办公小技巧分享

    1.8个免费PPT下载网站: (1)第1PPT:http://www.1ppt.com/moban/jianjie/ (2)优品PPT:http://www.ypppt.com/ (3)稻壳儿:https://www.docer.com/(需登录) (4)51PPT:http://www.51pptmoban.com/ (5)PPT宝藏:http://www.pptbz.com/ (6)当图网:https://www.99ppt.com/ (7)扑奔…...

    2024/3/29 7:07:31
  16. 聚类

    1. 生成数据 # 通过简单的例子来直接查看K均值聚类的效果 from sklearn.cluster import KMeans import matplotlib.pyplot as plt import numpy as np %matplotlib inline# 聚类前 X = np.random.rand(100, 2) plt.scatter(X[:, 0], X[:, 1], marker=o)2. sklearn结果 #聚类后 …...

    2024/3/29 7:07:30
  17. lectcode-pow(x,n)

    要求 实现 pow(x, n) ,即计算 x 的 n 次幂函数。 示例 示例 1: 输入: 2.00000, 10 输出: 1024.00000 示例 2: 输入: 2.10000, 3 输出: 9.26100 示例 3: 输入: 2.00000, -2 输出: 0.25000 解释: 2-2 = 1/22 = 1/4 = 0.25 代码直接暴力法double myPow(double x, int n) {double …...

    2024/3/29 7:07:29
  18. C++核心准则编译边学-F.45 不要返回右值引用

    F.45: Dont return a T&&F.45:不要返回右值引用Reason(原因)Its asking to return a reference to a destroyed temporary object. A && is a magnet for temporary objects.返回右值引用是在要求返回一个已经销毁的临时对象的引用。&&对于临时对象来…...

    2024/3/29 7:07:28
  19. 剑指offer-树的子结构[Java]

    题目描述 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) 解题思路 通过递归的方法来求A树的每一个子树和B树是否相等。 代码 public class Solution {public boolean HasSubtree(TreeNode root1,TreeNode root2) {if (root1 == null …...

    2024/3/29 7:16:03
  20. 查看Linux的CPU信息,核数等

    # 总核数 = 物理CPU个数 X 每颗物理CPU的核数 # 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数# 查看物理CPU个数 cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l# 查看每个物理CPU中core的个数(即核数) cat /proc/cpuinfo| grep "cp…...

    2024/4/30 10:32:08

最新文章

  1. Linux深入学习内核 - 中断与异常(下)

    软中断&#xff0c;Tasklet和Work Queue 由内核执行的几个任务之间有一些不是紧急的&#xff0c;他们可以被延缓一段时间&#xff01;把可延迟的中断从中断处理程序中抽出来&#xff0c;有利于使得内核保持较短的响应时间&#xff0c;所以我们现在使用以下面的这些结构&#x…...

    2024/5/3 4:08:29
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. Linux从入门到精通 --- 2.基本命令入门

    文章目录 第二章&#xff1a;2.1 Linux的目录结构2.1.1 路径描述方式 2.2 Linux命令入门2.2.1 Linux命令基础格式2.2.2 ls命令2.2.3 ls命令的参数和选项2.2.4 ls命令选项的组合使用 2.3 目录切换相关命令2.3.1 cd切换工作目录2.3.2 pwd查看当前工作目录2.4 相对路径、绝对路径和…...

    2024/5/1 13:38:20
  4. ElasticSearch的DSL查询

    ElasticSearch的DSL查询 准备工作 创建测试方法&#xff0c;初始化测试结构。 import org.apache.http.HttpHost; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRespo…...

    2024/5/2 7:02:52
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/2 9:28:15
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57