Ucore lab5

news2024/11/23 13:26:30

实验目的

  • 了解第一个用户进程创建过程
  • 了解系统调用框架的实现机制
  • 了解ucore如何实现系统调用sys_fork/sys_exec/sys_exit/sys_wait来进行进程管理

实验内容

练习0:已有实验代码改进

​本实验中完成了用户进程的创建,能够对用户进程进行基本管理,并为用户进程提供了必要的系统调用。为了支持用户进程,需要对已完成的实验代码进行改进。

1.proc_struct结构

​为了能够管理进程,进程控制块中新增加了变量,记录等待状态和退出原因,并将相关进程通过链表链接起来。

  • exit_code:记录进程的退出原因,这个值将传给等待的父进程
  • wait_state:标记当前进程是否处于等待状态
  • cptr:当前进程的子进程双向链表结点
  • yptr/optr:当前进程的older sibling和younger sibling的双向链表结点
struct proc_struct {
    enum proc_state state;                      // Process state
    int pid;                                    // Process ID
    int runs;                                   // the running times of Proces
    uintptr_t kstack;                           // Process kernel stack
    volatile bool need_resched;                 // bool value: need to be rescheduled to release CPU?
    struct proc_struct *parent;                 // the parent process
    struct mm_struct *mm;                       // Process's memory management field
    struct context context;                     // Switch here to run process
    struct trapframe *tf;                       // Trap frame for current interrupt
    uintptr_t cr3;                              // CR3 register: the base addr of Page Directroy Table(PDT)
    uint32_t flags;                             // Process flag
    char name[PROC_NAME_LEN + 1];               // Process name
    list_entry_t list_link;                     // Process link list 
    list_entry_t hash_link;                     // Process hash list
    int exit_code;                              // exit code (be sent to parent proc)
    uint32_t wait_state;                        // waiting state
    struct proc_struct *cptr, *yptr, *optr;     // relations between processes
};

2.alloc_proc

​由于进程控制块增加了新的变量,分配进程控制块时也需要进行相应的初始化。只需要在alloc_proc添加,将等待状态设为0,链表节点设为NULL。

    ... 
	proc->wait_state=0;
    proc->cptr = proc->optr = proc->yptr = NULL;
	...

3.do_fork

​do_fork中进行了进程的复制,对新的进程控制块的设置也需要补充。确认等待状态为0,只有在wait状态时进程的wait_state才会被设置为等待,一旦被唤醒,在wakeup_proc就会重新设置为0,此时应该不在等待状态。调用set_links将新进程和相关进程建立联系。补充后的do_fork如下:

    if((proc=alloc_proc())==NULL) {
    	goto fork_out;
    }
    assert(current->wait_state == 0);	//确保等待状态为0
    proc->parent = current;
    if(setup_kstack(proc)) {
    	goto bad_fork_cleanup_proc;
    }
    if(copy_mm(clone_flags,proc)) {
    	goto bad_fork_cleanup_kstack;
    }
    copy_thread(proc, stack, tf);		//复制上下文和中断帧
    //设置pid,加入进程列表,设置为可运行
    bool intr_flag=0;
    local_intr_save(intr_flag);
    {
    	proc->pid = get_pid();
    	hash_proc(proc);
    	set_links(proc);				//设置链表
    }
    local_intr_restore(intr_flag);
    wakeup_proc(proc);
    ret=proc->pid;

​set_links会将进程加入进程链表,设置父进程的子进程为自己,找到自己的older sibling进程,最后将进程数+1。

static void
set_links(struct proc_struct *proc) {
    list_add(&proc_list, &(proc->list_link));
    proc->yptr = NULL;
    if ((proc->optr = proc->parent->cptr) != NULL) {
        proc->optr->yptr = proc;
    }
    proc->parent->cptr = proc;
    nr_process ++;
}

4.idt_init

​引入用户进程后,需要用户进程能够进行系统调用,即可以发起中断,进行特权级切换。系统调用的中断号是128,需要单独设置该中断向量的特权级为用户特权级,这样用户就可以通过该中断发起系统调用。

void
idt_init(void) {
      extern uintptr_t __vectors[];
      int num=sizeof(idt)/sizeof(struct gatedesc);
      for(int i=0;i<num;i++){
      		SETGATE(idt[i],1,GD_KTEXT,__vectors[i],DPL_KERNEL);
      }
      //为T_SYSCALL设置用户态权限
      SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);	
      lidt(&idt_pd);
}

5.trap_disptach

​为了操作系统能正常进行进程调度,需要在时钟中断处,将进程的need_schedule设置为1,表示该进程时间配额已用完,需要调度运行其他程序。在trap调用trap_dispatch完成中断服务例程后,会判断这个值是否为1,然后调用need_schedule进行进程调度。

//trap_disptach
		case IRQ_OFFSET + IRQ_TIMER:
        ticks++;
        if(ticks%TICK_NUM==0) {
            assert(current != NULL);
            current->need_resched = 1;
        }
        break;
//trap中最后进行进程调度
void trap(struct trapframe *tf) {
    // dispatch based on what type of trap occurred
    // used for previous projects
    if (current == NULL) {
        trap_dispatch(tf);
    }
    else {
        // keep a trapframe chain in stack
        struct trapframe *otf = current->tf;
        current->tf = tf;
    
        bool in_kernel = trap_in_kernel(tf);			//是否是内核产生的中断
    
        trap_dispatch(tf);
    
        current->tf = otf;
        if (!in_kernel) {
            if (current->flags & PF_EXITING) {
                do_exit(-E_KILLED);
            }
            if (current->need_resched) {				//判断是否需要调度
                schedule();
            }
        }
    }
}

练习1: 加载应用程序并执行

1.从内核线程到用户进程

​Lab4中已经实现了内核线程的创建,能够在内核态运行线程。在Lab5中需要实现用户进程的创建,并为用户进程提供一些系统调用,并对用户进程的执行进行基本的管理。

​进程运行在用户态,有自己的地址空间。与内核相比,进程管理和内存管理这两个部分有很大的不同。

进程管理

​在进程管理方面,操作系统主要需要实现的有:建立进程的页表和维护进程可访问空间;加载ELF格式的程序到进程控制块管理的内存中的方法;在进程复制(fork)过程中,把父进程的内存空间拷贝到子进程内存空间的技术。此外还需要实现与用户态进程生命周期管理相关的:让进程放弃CPU而睡眠等待某事件;让父进程等待子进程结束;一个进程杀死另一个进程;给进程发消息;建立进程的关系链表。

内存管理

​在内存管理方面,操作系统主要是需要维护进程的地址空间,即维护用户进程的页表,维护地址空间到物理内存的映射。不同的进程有各自的页表,即便不同进程的用户态虚拟地址相同,由于页表把虚拟页映射到了不同的物理页帧,不同进程的地址空间也不同,且相互隔离开。此外,在用户态内存空间和内核态内核空间之间需要拷贝数据,让CPU处在内核态才能完成对用户空间的读或写,为此需要设计专门的拷贝函数(copy_from_user和copy_to_user)完成。

从内核线程到用户进程

​在proc_init中,会建立第1个内核线程idle_proc,这个线程总是调度运行其他线程。然后proc_init会调用kernel_thread建立init_main线程,接着在init_main中将调用kernel_thread建立user_main线程。user_main仍然是一个内核线程,但他的任务是创建用户进程。在user_main中将调用KERNEL_EXECVE,从而调用kernel_execve来把某一具体程序(exit)的执行内容放入内存,覆盖user_main线程,此后就可以调度执行程序,该程序在用户态运行,此时也就完成了用户进程的创建。

//在user_main中调用KERNEL_EXECVE,覆盖掉user_main,创建用户进程
static int
user_main(void *arg) {
#ifdef TEST
    KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE);
#else
    KERNEL_EXECVE(exit);
#endif
    panic("user_main execve failed.\n");
}

2.加载应用程序

(1).产生中断

​在user_main中,将调用KERNEL_EXECVE2加载用户程序,将该程序的内存空间替换掉当前线程的内存空间,将当前内核线程转变为要执行的进程。加载过程的第一步是由KERNEL_EXECVE2调用kernel_execve,发起系统调用。

static int
kernel_execve(const char *name, unsigned char *binary, size_t size) {
    int ret, len = strlen(name);
    asm volatile (
        "int %1;"
        : "=a" (ret)
        : "i" (T_SYSCALL), "0" (SYS_exec), "d" (name), "c" (len), "b" (binary), "D" (size)
        : "memory");
    return ret;
}

#define __KERNEL_EXECVE(name, binary, size) ({                          \
            cprintf("kernel_execve: pid = %d, name = \"%s\".\n",        \
                    current->pid, name);                                \
            kernel_execve(name, binary, (size_t)(size));                \
        })

#define KERNEL_EXECVE(x) ({                                             \
            extern unsigned char _binary_obj___user_##x##_out_start[],  \
                _binary_obj___user_##x##_out_size[];                    \
            __KERNEL_EXECVE(#x, _binary_obj___user_##x##_out_start,     \
                            _binary_obj___user_##x##_out_size);         \
        })

​由于此时还没有建立文件系统,需要执行的用户程序是随ucore的kernel直接加载到内存中的,并使用全局变量记录了这段用户程序代码的起始位置和大小。从宏定义调用kernel_execve会将程序名,位置和大小都传入。kernel_execve将这些信息保存到指定的寄存器中,发起中断,进行系统调用,具体的细节在练习三中进行分析。中断进行系统调用时的调用顺序如下:

vector128(vectors.S)--\>
\_\_alltraps(trapentry.S)--\>trap(trap.c)--\>trap\_dispatch(trap.c)--
--\>syscall(syscall.c)--\>sys\_exec(syscall.c)--\>do_execve(proc.c)

​最终系统调用将通过do_execve完成用户程序的加载。

//syscall.c,exec系统调用
static int
sys_exec(uint32_t arg[]) {
    const char *name = (const char *)arg[0];
    size_t len = (size_t)arg[1];
    unsigned char *binary = (unsigned char *)arg[2];
    size_t size = (size_t)arg[3];
    return do_execve(name, len, binary, size);
}
(2).do_execve

​接下来分析do_execve是怎样加载处于内存中的程序并建立好用户内存空间,并设置中断帧,完成用户进程创建并执行用户程序的。传入的参数为用户程序名和长度,用户程序代码位置和大小。do_execve完整的实现如下:

int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
    struct mm_struct *mm = current->mm;
    if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
        return -E_INVAL;
    }
    if (len > PROC_NAME_LEN) {
        len = PROC_NAME_LEN;
    }

    char local_name[PROC_NAME_LEN + 1];
    memset(local_name, 0, sizeof(local_name));
    memcpy(local_name, name, len);
	/*清空内存空间*/
    if (mm != NULL) {
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {	//调用load_icode加载用户程序并完成后续工作
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

​进入do_execve后先进行程序名字长度的调整,然后就开始使用新进程覆盖原进程。首先,由于新进程将使用新的用户内存空间,原进程的内存空间需要进行清空。如果mm_struct为空,则原进程是内核线程,不需要处理。如果mm_struct不为空,设置页表为内核空间页表,将引用计数-1,如果引用计数为0则根据mm_struct记录的信息对原进程的内存空间进行释放。

//do_execve中将原内存空间清空
	if (mm != NULL) {
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }
//exit_mmap调用unmap_range,exit_range取消地址映射
void exit_mmap(struct mm_struct *mm) {
    assert(mm != NULL && mm_count(mm) == 0);
    pde_t *pgdir = mm->pgdir;
    list_entry_t *list = &(mm->mmap_list), *le = list;
    while ((le = list_next(le)) != list) {
        struct vma_struct *vma = le2vma(le, list_link);
        unmap_range(pgdir, vma->vm_start, vma->vm_end);
    }
    while ((le = list_next(le)) != list) {
        struct vma_struct *vma = le2vma(le, list_link);
        exit_range(pgdir, vma->vm_start, vma->vm_end);
    }
}
//put_pgdir释放页目录占用的内存空间
static void put_pgdir(struct mm_struct *mm) {
    free_page(kva2page(mm->pgdir));
}
//mm_destroy销毁mm_struct
void mm_destroy(struct mm_struct *mm) {
    assert(mm_count(mm) == 0);

    list_entry_t *list = &(mm->mmap_list), *le;
    while ((le = list_next(list)) != list) {
        list_del(le);
        kfree(le2vma(le, list_link));  //kfree vma        
    }
    kfree(mm); //kfree mm
    mm=NULL;
}

​接下来的工作就是加载elf格式的用户程序,申请新的用户内存空间,并设置中断帧,使用户进程最终可以运行。这些工作都是由load_icode函数完成的。

(3).load_icode

​load_icode完成了以下6个工作:

  • 为新进程创建mm结构
  • 创建新的页目录,并把内核页表复制到新创建的页目录,这样新进程能够正确映射内核空间
  • 分配内存,从elf文件中复制代码和数据,初始化.bss段
  • 建立用户栈空间
  • 将新进程的mm结构设置为刚刚创建的mm
  • 构造中断帧,使用户进程最终能够正确在用户态运行

​前两个工作比较简单,只需要调用mm_create与setup_pgdir,完成mm结构的创建与新的页目录的创建,创建失败则需要将已创建的mm和页目录进行销毁。

static int load_icode(unsigned char *binary, size_t size) {
	if (current->mm != NULL) {
        panic("load_icode: current->mm must be empty.\n");
    }

    int ret = -E_NO_MEM;
    struct mm_struct *mm;
    //创建一个mm_struct给用户程序使用
    if ((mm = mm_create()) == NULL) {
        goto bad_mm;
    }
    //创建新的PDT,并把内核页表的内容复制到新的页目录
    if (setup_pgdir(mm) != 0) {
        goto bad_pgdir_cleanup_mm;
    }

​第三步主要是创建虚拟内存空间vma,根据elf文件头的信息复制代码段和数据段的数据,并将vma插入mm结构中,表示合法的用户虚拟空间。

    //(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
    struct Page *page;
    //elf文件的ELF头部
    struct elfhdr *elf = (struct elfhdr *)binary;
    //确定elf文件的program section headers
    struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
    //确认为有效的elf文件
    if (elf->e_magic != ELF_MAGIC) {
        ret = -E_INVAL_ELF;
        goto bad_elf_cleanup_pgdir;
    }
	//开始创建虚拟空间并复制数据
    uint32_t vm_flags, perm;
    struct proghdr *ph_end = ph + elf->e_phnum;
    for (; ph < ph_end; ph ++) {
    	//遍历每个program section headers
        if (ph->p_type != ELF_PT_LOAD) {
            continue ;						//不是需要加载的段跳过
        }
        if (ph->p_filesz > ph->p_memsz) {
            ret = -E_INVAL_ELF;				//大小不正确
            goto bad_cleanup_mmap;
        }
        if (ph->p_filesz == 0) {			//段大小为0,跳过
            continue ;
        }
    	//调用mm_map进行vma的建立
        vm_flags = 0, perm = PTE_U;
        if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
        if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
        if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
        if (vm_flags & VM_WRITE) perm |= PTE_W;
        //建立合法vma并插入mm结构维护的链表
        if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
            goto bad_cleanup_mmap;
        }
        unsigned char *from = binary + ph->p_offset;
        size_t off, size;
        uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
        ret = -E_NO_MEM;
		//已建立了合法的vma,接下来分配物理内存
        end = ph->p_va + ph->p_filesz;
    	//加载elf文件中的数据
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {		//分配页
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memcpy(page2kva(page) + off, from, size);							//数据复制
            start += size, from += size;
        }

      	//设置.bss段
        end = ph->p_va + ph->p_memsz;
        if (start < la) {
            /* ph->p_memsz == ph->p_filesz */
            if (start == end) {
                continue ;
            }
            off = start + PGSIZE - la, size = PGSIZE - off;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);								//bss段清0
            start += size;
            assert((end < la && start == end) || (end >= la && start == la));
        }
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);								//bss段清0
            start += size;
        }
    }

​接下来的第四步和第五步是建立用户栈,为用户栈设立合法虚拟空间,然后将已经设置好的mm设置为当前进程的mm。

    //建立用户栈,设置合法虚拟空间
    vm_flags = VM_READ | VM_WRITE | VM_STACK;
    if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
        goto bad_cleanup_mmap;
    }
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
    //设置当前的mm,cr3
    mm_count_inc(mm);
    current->mm = mm;
    current->cr3 = PADDR(mm->pgdir);
    lcr3(PADDR(mm->pgdir));

​最后一步就是构造用户进程的中断帧,在load_icode、sys_exec函数返回,中断结束后,从中断帧恢复寄存器后回到用户态,降低特权级,能够执行用户进程的程序。中断帧中,cs,ds,ss,es寄存器设置为用户代码段和数据段的段寄存器,esp设置为用户栈的栈顶,eip设置为用户程序的入口,最后设置标志位,使用户进程可以被中断,这样中断帧就设置好了用户态下用户进程运行的环境。这一步是练习一中要求补全的部分,代码如下:

    struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    tf->tf_cs = USER_CS;
   	tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
    tf->tf_esp = USTACKTOP;
    tf->tf_eip = elf->e_entry;
    tf->tf_eflags = FL_IF; 
    ret = 0;

3.应用程序的运行

​通过上述do_execve中的操作,原来的user_main已经被用户进程所替换掉了。此时处于RUNNABLE状态的是已经创建完成了的用户进程,系统调用已经完成,将按照调用的顺序一路返回到__trapret,从中断帧中恢复寄存器的值,通过iret回到用户进程exit的第一条语句(initcode.S中的_start)开始执行。

​综上所述,一个用户进程创建到执行第一条指令的完整过程如下:

  • 父进程通过fork系统调用创建子进程。通过do_fork进行进程资源的分配,创建出新的进程
  • fork返回0,子进程创建完成,等待调度。fork中将进程设置为RUNNABLE,该进程可以运行schedule函数进行调度,调用proc_run运行该进程
  • 该进程调用kernel_execve,产生中断并进行exec系统调用
  • do_execve将当前进程替换为需要运行的用户进程,加载程序并设置好中断帧
  • 从中断帧返回到用户态,根据中断帧中设置的eip,跳转执行用户程序的第一条指令

练习2:复制父进程的内存

1.复制父进程的内存

​在Lab4中已经分析过了do_fork函数对创建的进程的资源的分配,其中内存资源的分配是由copy_mm完成的,Lab4创建内核线程,因此没有进行内存的复制,在本实验中,copy_mm将为新进程分配内存空间,并将父进程的内存资源复制到新进程的内存空间。

(1)copy_mm

​对于共享内存的线程或进程,不需要进行复制,根据clone_flags判断是共享时,可以直接返回父进程的mm。而不共享的情况下,首先创建一个mm_struct,调用setup_pgdir创建新的页目录,并将内核页目录复制到新的页目录,然后调用dup_mmap进行内存资源的复制。

static int
copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
    struct mm_struct *mm, *oldmm = current->mm;
    /* current is a kernel thread */
    if (oldmm == NULL) {
        return 0;
    }
    if (clone_flags & CLONE_VM) {
        mm = oldmm;
        goto good_mm;
    }
    int ret = -E_NO_MEM;
    if ((mm = mm_create()) == NULL) {
        goto bad_mm;
    }
    if (setup_pgdir(mm) != 0) {
        goto bad_pgdir_cleanup_mm;
    }
    lock_mm(oldmm);						//定义在vmm.h中,进行上锁
    {
        ret = dup_mmap(mm, oldmm);		//内存资源的复制
    }
    unlock_mm(oldmm);
    if (ret != 0) {
        goto bad_dup_cleanup_mmap;
    }

good_mm:
    mm_count_inc(mm);
    proc->mm = mm;
    proc->cr3 = PADDR(mm->pgdir);
    return 0;
bad_dup_cleanup_mmap:
    exit_mmap(mm);
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    return ret;
}
(2)dup_mmap

​dup_mmap中,通过遍历mm_struct中的链表,给子进程分配所有父进程拥有的vma虚拟空间,将创建的vma插入mm中,并调用copy_range将父进程vma中的数据复制到子进程新创建的vma中。

//vmm.c中定义的dup_mmap
int
dup_mmap(struct mm_struct *to, struct mm_struct *from) {
    assert(to != NULL && from != NULL);
    list_entry_t *list = &(from->mmap_list), *le = list;
    while ((le = list_prev(le)) != list) {
        struct vma_struct *vma, *nvma;
        vma = le2vma(le, list_link);
        nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags);	//创建vma
        if (nvma == NULL) {
            return -E_NO_MEM;
        }
        insert_vma_struct(to, nvma);									//将新创建的vma插入mm
		//进行复制
        bool share = 0;
        if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) {
            return -E_NO_MEM;
        }
    }
    return 0;
}
(3)copy_range

​copy_range中会把父进程的vma中的内容复制给子进程的vma。父进程和子进程的vma相同,但映射到的物理页不同。首先找到父进程的vma对应的页表项,从该页表项可以找到父进程的vma对应的物理页,然后为子进程创建页表项并分配新的一页,接下来将父进程vma物理页中的数据复制到子进程新分配出的一页,再调用page_insert将子进程的新页的页表项设置好,建立起虚拟地址到物理页的映射,这个vma的复制工作就完成了。具体的数据复制使用memcpy函数,需要传入虚拟地址,但此时处于内核态,因此还要把物理页的地址转换为该页对应内核的虚拟地址。这是练习二需要完成的部分,最终copy_range完整的实现如下:

int
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    assert(start % PGSIZE == 0 && end % PGSIZE == 0);
    assert(USER_ACCESS(start, end));
    //按页复制
    do {
        //找到父进程的页表项
        pte_t *ptep = get_pte(from, start, 0), *nptep;
        if (ptep == NULL) {
            start = ROUNDDOWN(start + PTSIZE, PTSIZE);
            continue ;
        }
        //建立新进程的页表项
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
        	uint32_t perm = (*ptep & PTE_USER);
        	//获取父进程的物理页
        	struct Page *page = pte2page(*ptep);
        	//为新进程分配物理页
        	struct Page *npage=alloc_page();
        	assert(page!=NULL);
        	assert(npage!=NULL);
     		int ret=0;
			//得到页的内核虚拟地址后使用memcopy复制
	 		void *src_kva=page2kva(page);
	 		void *dst_kva=page2kva(npage);			
	 		memcpy(dst_kva,src_kva,PGSIZE);
	 		ret = page_insert(to,npage,start,perm);
            assert(ret == 0);
        }
        start += PGSIZE;
    } while (start != 0 && start < end);
    return 0;
}

2.Copy-on-Write

​Copy on Write 是读时共享,写时复制机制。多个进程可以读同一部分数据,需要对数据进行写时再复制一份到自己的内存空间。具体的实现为,在fork时,直接将父进程的地址空间即虚拟地址复制给子进程,不分配实际的物理页给子进程,并将父进程所有的页都设置为只读。父子进程都可以读取该页,当父子进程写该页时,就会触发页访问异常,发生中断,调用中断服务例程,在中断服务例程中,将触发异常的虚拟地址所在的页复制,分配新的一页存放数据,这样父子进程写该部分数据时就各自可以拥有一份自己的数据。

​大概的实现思路为:

  • 复制父进程内存时直接将父进程的物理页映射到子进程的虚拟页,且父子进程的该页表项均修改为只读。(修改copy_range)
  • 当父子进程需要写时,会触发页访问异常,在页访问异常中进行内存页的分配和复制(修改do_pgfault)

练习3:fork/exec/wait/exit 的实现及系统调用

1.系统调用

​用户进程在用户态下运行,不能执行特权指令,如果需要执行特权指令,只能通过系统调用切换到内核态,交给操作系统来完成。

用户库

​为了简化应用程序进行系统调用方式,用户库中提供了对系统调用的封装。即只需要在程序中通过调用如exit,fork,wait等库函数,库函数将进行系统调用的发起。在ucore中,这部分封装放在user/libs/ulib.c中

void exit(int error_code) {
    sys_exit(error_code);
    cprintf("BUG: exit failed.\n");
    while (1);
}

int fork(void) {
    return sys_fork();
}

int wait(void) {
    return sys_wait(0, NULL);
}
......

​最终这些库函数都会调用syscall.c中的syscall,只是传入的参数不同,在该函数中使用内联汇编直接发起中断,中断号为定义的T_SYSCALL(0x80),即系统调用为128号中断。进行中断调用时会向eax寄存器传入参数,这个参数表示发生了具体哪个系统调用,同时还可以根据需要传入最多5个参数,分别传入edx,ecx,ebx,edi和esi寄存器中。

libs/unistd.h:#define T_SYSCALL           0x80
static inline int
syscall(int num, ...) {
    va_list ap;
    va_start(ap, num);
    uint32_t a[MAX_ARGS];
    int i, ret;
    for (i = 0; i < MAX_ARGS; i ++) {
        a[i] = va_arg(ap, uint32_t);
    }
    va_end(ap);

    asm volatile (
        "int %1;"
        : "=a" (ret)
        : "i" (T_SYSCALL),
          "a" (num),
          "d" (a[0]),
          "c" (a[1]),
          "b" (a[2]),
          "D" (a[3]),
          "S" (a[4])
        : "cc", "memory");
    return ret;
}

int sys_exit(int error_code) {
    return syscall(SYS_exit, error_code);
}

系统中断

​用户态下发起中断后,就可以跳转执行对应的中断服务例程,而中断服务例程的地址保存在idt表中,idt表在内核启动后的kern_init中调用idt_init进行初始化,这是在lab1中已经完成的。设置中断服务例程时,进入中断服务例程的特权级均设置为内核特权级,本实验中,系统调用由用户发起,因此需要单独设置中断表idt中128号中断描述符的特权级为用户特权级,这样用户就可以通过中断提升特权级,进行系统调用。

void
idt_init(void) {
	extern uintptr_t __vectors[];
	int i = 0;
	for (i = 0; i < (sizeof(idt) / sizeof(struct gatedesc)); i++) {
		SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
	}
    //为T_SYSCALL设置用户态权限,类型为系统调用
	SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
	lidt(&idt_pd);
}

​根据设置好的中断描述符就可以切换特权级,进入系统调用对应的中断服务例程了。在进行特权级切换及进入中断服务历程之前,首先在__alltraps完成中断帧trapframe的建立,在执行完trap后会回到__alltraps,在__trapret中从中断帧恢复寄存器的值,中断返回。

.globl vector128
vector128:
  pushl $0
  pushl $128
  jmp __alltraps
//建立trapframe
.globl __alltraps
__alltraps:
    # push registers to build a trap frame
    # therefore make the stack look like a struct trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

    # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

    # push %esp to pass a pointer to the trapframe as an argument to trap()
    pushl %esp

    # call trap(tf), where tf=%esp
    call trap

​接下来将调用trap,并在trap中根据情况调用trap_dispatch,trap_dispatch中根据trapframe中的tf_trapno进行相应的处理,这个值是一开始就被压入栈中的中断号128,将调用syscall。

//trap_dispatch
switch (tf->tf_trapno) {
	...
    case T_SYSCALL:
        syscall();
        break;
    ...
}

​在syscall中,将根据发出中断调用时传入eax寄存器的值判断系统调用具体类型,调用对应的函数。

void
syscall(void) {
    struct trapframe *tf = current->tf;
    uint32_t arg[5];
    int num = tf->tf_regs.reg_eax;
    if (num >= 0 && num < NUM_SYSCALLS) {
        if (syscalls[num] != NULL) {
            arg[0] = tf->tf_regs.reg_edx;
            arg[1] = tf->tf_regs.reg_ecx;
            arg[2] = tf->tf_regs.reg_ebx;
            arg[3] = tf->tf_regs.reg_edi;
            arg[4] = tf->tf_regs.reg_esi;
            tf->tf_regs.reg_eax = syscalls[num](arg);
            return ;
        }
    }
    print_trapframe(tf);
    panic("undefined syscall %d, pid = %d, name = %s.\n",
            num, current->pid, current->name);
}

​ucore一共提供了以下这些系统调用:

static int (*syscalls[])(uint32_t arg[]) = {
    [SYS_exit]              sys_exit,
    [SYS_fork]              sys_fork,
    [SYS_wait]              sys_wait,
    [SYS_exec]              sys_exec,
    [SYS_yield]             sys_yield,
    [SYS_kill]              sys_kill,
    [SYS_getpid]            sys_getpid,
    [SYS_putc]              sys_putc,
    [SYS_pgdir]             sys_pgdir,
};

​而这些函数最终会调用do_fork,do_exit等函数完成需要完成的任务,然后返回值存放在eax寄存器中,一路返回到__trapret,从中断栈恢复寄存器的值,回到用户态,中断结束,继续正常运行进程。

2.fork

​fork用于创建新的进程。进程调用fork函数,将通过系统调用,创建一个与原进程相同的进程,该进程与原进程内存相同,执行相同的代码,但有自己的地址空间。对于父进程,fork返回子进程的pid,创建出的子进程从fork返回0。一次具体的fork调用从调用fork用户库函数开始,调用包装好的fork。

int fork(void) {
    return sys_fork();
}

​包装好的用户库函数将进一步调用sys_fork,在sys_fork中将调用syscall,传入SYS_fork,即系统调用类型:

int
sys_fork(void) {
    return syscall(SYS_fork);
}

​在syscall中将发起中断,传入相关参数,通过int指令发起128号中断。发生中断首先进行用户栈到特权栈的切换,在__alltraps函数中建立trapframe,然后call trap,进行中断处理,中断处理将使用中断号128,从中断表中进入对应的中断服务例程即syscall系统调用,在syscall中,根据传入的SYS_fork确定系统调用的具体类型,然后就将进入对应的系统调用函数:

static int (*syscalls[])(uint32_t arg[]) = {
    [SYS_exit]              sys_exit,
    [SYS_fork]              sys_fork,
    [SYS_wait]              sys_wait,
    [SYS_exec]              sys_exec,
    [SYS_yield]             sys_yield,
    [SYS_kill]              sys_kill,
    [SYS_getpid]            sys_getpid,
    [SYS_putc]              sys_putc,
    [SYS_pgdir]             sys_pgdir,
};

​最终将进入do_fork进行进程的复制,该函数在Lab4中已经完成。该函数将创建一个新的进程控制块管理新的进程,然后调用copy_mm,copy_thread等函数给新的进程分配资源,并复制父进程的内存,在copy_thread中复制父进程的上下文和中断帧时,设置中断帧的eax值为0,这样复制出的子进程在将来返回时将返回0,且eip设置为forkret,调度运行子进程时,会进行上下文切换进入forkret,然后从中断帧恢复寄存器,返回0。

static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
	proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1; //内核栈顶
	*(proc->tf) = *tf;
	proc->tf->tf_regs.reg_eax = 0; 				//子进程返回0
	proc->tf->tf_esp = esp; 					//父进程的用户栈指针
	proc->tf->tf_eflags |= FL_IF; 				//设置能够响应中断
	proc->context.eip = (uintptr_t)forkret; 	//返回
	proc->context.esp = (uintptr_t)(proc->tf); 	//trapframe
}

​而父进程将返回子进程的pid。

//do_fork返回
	...
	ret=proc->pid;
fork_out:
    return ret;
	...

​完成子进程的创建工作之后,将从do_fork按调用顺序返回至__trapret,从trapframe恢复状态,返回到用户库的syscall,最后返回用户程序调用fork处继续执行下一条语句。此时,父子进程同时存在,此后如果发生调度,子进程也将通过上下文切换,从forkret返回__trapret,最后返回到用户程序的下一条语句。

​完整的一次fork调用的调用顺序如下:

fork-->sys_fork-->syscall-->int 0x80发起128号中断-->__alltraps-->trap_dispatch-->syscall-->sys_fork
-->do_fork

​完成调用后,父进程状态不变,子进程创建成功,为可运行状态,等待调度。

3.exec

​在本实验的ucore代码中,没有提供用户库包装的exec,如果编写类似fork的包装,调用情况与fork是完全相同的。本实验中,直接在user_main中使用宏定义发起中断,最终调用do_execve,将user_main替换为exit.c中的用户程序,调用顺序如下:

user_main-->KERNEL_EXECVE-->__KERNEL_EXECVE-->kernel_execve-->int 0x80发起128号中断-->__alltraps-->trap_dispatch-->syscall-->sys_exec-->do_execve

​在do_execve调用的load_icode中,对中断帧进行了设置,将eip设置为了elf文件中给出的程序入口,即用户程序的入口,_start。接下来将调用umain,从umain进入exit.c的main中开始执行程序。

//_start
.text
.globl _start
_start:
    # set ebp for backtrace
    movl $0x0, %ebp

    # move down the esp register
    # since it may cause page fault in backtrace
    subl $0x20, %esp

    # call user-program function
    call umain
//umain
int main(void);
void
umain(void) {
    int ret = main();
    exit(ret);
}

​通过exec(在本实验中是KERNEL_EXECVE),当前正在执行的进程可以发起系统调用,然后通过do_execve,创建一个新的进程,建立完全不同的地址空间,从elf文件中加载代码和数据信息,进行好加载工作后,设置中断帧,使中断返回时能返回到新的进程的程序入口,这样返回后就开始执行新的程序。通过这一系列工作,这个新的程序就可以将原来的程序替换掉。看起来进程只是发生了一次系统调用,但系统调用结束后,进程的地址空间,代码,数据等已经完全被替换掉,然后开始正常运行新的程序。

4.wait

​wait函数用于让当前进程等待他的子进程结束。ucore提供了用户库包装后的wait和waitpid,wait是使用默认参数的waitpid,即等待任意进程结束。这里对waitpid进行分析,waitpid调用的过程与fork类似:

waitpid-->sys_wait-->syscall-->int 0x80发起128号中断-->__alltraps-->trap_dispatch-->syscall-->sys_wait-->do_wait

​最终将调用系统调用函数do_wait,do_wait中会寻找是否有子进程为僵尸态(PROC_ZOMBIE),如果没有则将运行schedule调度其他进程运行,当前进程睡眠(PROC_SLEEPING),当有子进程运行结束转变为僵尸态,这个进程将被唤醒,从进程链表删除子进程,并将子进程的进程控制块也释放,彻底结束子进程,然后返回。传入的参数为0则等待任意子进程结束,否则等待指定的子进程结束。

int
do_wait(int pid, int *code_store) {
    struct mm_struct *mm = current->mm;
    if (code_store != NULL) {							//存放导致子进程退出的退出码
        if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
            return -E_INVAL;
        }
    }
    struct proc_struct *proc;
    bool intr_flag, haskid;
repeat:
    haskid = 0;
    if (pid != 0) {										//等待指定pid的子进程
        proc = find_proc(pid);								
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;
            }
        }
    }
    else {
        proc = current->cptr;							//等待任意子进程
        for (; proc != NULL; proc = proc->optr) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;
            }
        }
    }
    if (haskid) {
        current->state = PROC_SLEEPING;					//进入睡眠状态
        current->wait_state = WT_CHILD;					//等待状态-等待子进程
        schedule();										//调度
        if (current->flags & PF_EXITING) {				//如果当前进程已经结束,do_exit
            do_exit(-E_KILLED);
        }
        goto repeat;
    }
    return -E_BAD_PROC;

found:
    if (proc == idleproc || proc == initproc) {
        panic("wait idleproc or initproc.\n");			//不可以等待init_proc和idle_proc结束
    }
    if (code_store != NULL) {
        *code_store = proc->exit_code;
    }
    local_intr_save(intr_flag);
    {
        unhash_proc(proc);								//从进程链表中删除
        remove_links(proc);
    }
    local_intr_restore(intr_flag);
    put_kstack(proc);									//释放内核栈
    kfree(proc);										//释放进程块
    return 0;
}

​调用waitpid,当前进程将等待子进程运行结束,未结束时,当前进程将进入睡眠状态,直到子进程结束。等到了子进程的结束,do_wait中会将子进程从进程链表删除,让子进程彻底结束。

5.exit

​exit用于退出并结束当前进程,也已经进行了包装,用户程序可以直接调用。

void exit(int error_code) {
    sys_exit(error_code);
    cprintf("BUG: exit failed.\n");
    while (1);
}

​调用过程和fork,waitpid类似,最终调用do_exit。

exit-->sys_exit-->syscall-->int 0x80发起128号中断-->__alltraps-->trap_dispatch-->syscall-->sys_exit-->do_exit

​在do_exit中,该进程的内存资源将被释放,同时状态将被设置为PROC_ZOMBIE,最后从进程链表删除该进程由他的父进程来完成,因此会判断其父进程是否在等待,如果等待则将父进程唤醒。最后还要处理该进程的子进程,因为他结束后无法处理自己的子进程,就遍历链表将子进程全部设置为init_proc的子进程,让init_proc完成PROC_ZOMBIE状态的子进程最后的处理。

int
do_exit(int error_code) {
    if (current == idleproc) {
        panic("idleproc exit.\n");
    }
    if (current == initproc) {
        panic("initproc exit.\n");
    }
    //释放内存
    struct mm_struct *mm = current->mm;
    if (mm != NULL) {									//mm==NULL是内核线程
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);								//取消映射
            put_pgdir(mm);								//删除页表
            mm_destroy(mm);								//删除mm
        }
        current->mm = NULL;
    }
    current->state = PROC_ZOMBIE;						//PROC_ZOMBIE状态
    current->exit_code = error_code;
    
    bool intr_flag;
    struct proc_struct *proc;
    local_intr_save(intr_flag);
    {
        proc = current->parent;
        if (proc->wait_state == WT_CHILD) {
            wakeup_proc(proc);							//唤醒父进程(父进程等待)
        }
        while (current->cptr != NULL) {					//将退出进程的子进程的父进程设置为init_proc
            proc = current->cptr;
            current->cptr = proc->optr;
    
            proc->yptr = NULL;
            if ((proc->optr = initproc->cptr) != NULL) {
                initproc->cptr->yptr = proc;
            }
            proc->parent = initproc;
            initproc->cptr = proc;
           	//如果子进程已经为PROC_ZOMBIE且init_proc在等待,唤醒init_proc
            if (proc->state == PROC_ZOMBIE) {			
                if (initproc->wait_state == WT_CHILD) {
                    wakeup_proc(initproc);
                }
            }
        }
    }
    local_intr_restore(intr_flag);
    
    schedule();											//调度其他进程
    panic("do_exit will not return!! %d.\n", current->pid);
}

​综上所述,调用exit会让当前进程结束,释放所有的内存资源,但这个进程将仍以PROC_ZOMBIE状态存在,等待父进程做最后的处理,并且该进程结束前也会把自己的子进程交给init_proc,确保自己的子进程也可以最终被彻底结束,然后就调用schedule,调度运行其他进程。

6.用户态进程的生命周期

​用户态进程的生命周期可用下图表示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAGTmkWq-1682902142500)(进程生命周期.jpg)]

一个进程首先由父进程fork产生,状态会由刚分配进程控制块的UNINIT状态转变为RUNNABLE状态,为就绪状态。当发生调度选中次进程时,调度程序调用proc_run切换到该进程,该进程进入运行态。此后子进程可以通过execve发起系统调用,将自己替换为用户程序,但进程状态不会发生改变。父进程可以通过wait发起系统调用,将自己转变为SLEEPING休眠态,等待子进程的完成。子进程运行结束后将变为ZOMBIE僵尸态,而父进程将被唤醒,进行子进程资源的回收。此后父进程回到运行态(或就绪态),而子进程已彻底结束。

实验总结

重要知识点

  • 用户进程的创建过程
  • 加载用户程序的过程
  • fork对父进程内存资源的复制
  • copy-on-write机制
  • 系统调用

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

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

相关文章

C语言入门篇——自定义数据篇

目录 1、结构体 1.2、匿名结构体 1.3、结构体的自引用 1.4、结构体的声明和初始化 1.5、结构体的内存对齐 1.6、修改默认对齐数 1.7、结构体传参 2、枚举 3、共用体&#xff08;联合体&#xff09; 1、结构体 设计程序时&#xff0c;最重要的步骤之一是选择表示数据的…

【微机原理】8088/8086微处理器

目录 一、8088/8086的功能结构 1.总线接口部件&#xff08;BIU&#xff09; 2.执行部件&#xff08;EU&#xff09; 二、8088/8086的寄存器结构&#xff08;14个&#xff09; 溢出标志的概念 溢出和进位的区别 8086CPU是Intel系列的16位微处理器&#xff0c;他有16根数据…

框架学习之KOCA框架简介

KOCA框架简介 什么是KOCA术语定义发展历史 KOCA的总体架构产品优势开放性敏捷性&#xff08;一体化解决方案&#xff09;融合性安全性接入网关- KOCA Gateway KOCA DevOps流水线 KOCA技术栈 金证开发者社区&#xff1a;http://koca.szkingdom.com/ 什么是KOCA KOCA是金证基于…

LC-1376. 通知所有员工所需的时间(DFS:自上而下、自下而上)

1376. 通知所有员工所需的时间 难度中等125 公司里有 n 名员工&#xff0c;每个员工的 ID 都是独一无二的&#xff0c;编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。 在 manager 数组中&#xff0c;每个员工都有一个直属负责人&#xff0c;其中 manager[i] 是第…

JavaScript常用数组方法-汇总

快速检索 方法解析 1&#xff1a;concat(); 功能&#xff1a;合并数组&#xff0c;可以合并一个或多个数组&#xff0c;会返回合并数组之后的数据&#xff0c;不会改变原来的数组&#xff1b; var str1 [12,2,"hello"];var str2 ["world"]; console.lo…

简单毛概刷题网页制作 2.0(拖欠近一年版)

原因是大概一年之前学校的毛概期末刷题网站突然崩了&#xff0c;但是一直没有修复。当时眼看着复习时间逐渐被压缩&#xff0c;自己啥也做不了&#xff0c;遂自学前端完成毛概刷题网页一枚。 最早的毛概刷题网站仅仅是 1.0 版本&#xff08;传送门&#xff09;&#xff0c;功能…

Excel技能之对齐,你可能都没想到

Excel表格&#xff0c;既然要做得漂漂亮亮&#xff0c;一定离不开对齐。拍照需要美颜&#xff0c;表格需要对齐。 内容全部挤到一边去。 有些靠左&#xff0c;有些靠右。 加上空格&#xff0c;感觉对齐。如果数据特别多&#xff0c;又逃不过加班的命运。 实在是混乱不堪。审美…

Linux常用命令——iostat命令

在线Linux命令查询工具 iostat 监视系统输入输出设备和CPU的使用情况 补充说明 iostat命令被用于监视系统输入输出设备和CPU的使用情况。它的特点是汇报磁盘活动统计情况&#xff0c;同时也会汇报出CPU使用情况。同vmstat一样&#xff0c;iostat也有一个弱点&#xff0c;就…

Linux安装MongoDB数据库,并内网穿透远程连接

文章目录 前言1. 配置Mongodb源2. 安装MongoDB3. 局域网连接测试4. 安装cpolar内网穿透5. 配置公网访问地址6. 公网远程连接7. 固定连接公网地址8. 使用固定地址连接 转载自Cpolar Lisa文章&#xff1a;Linux服务器安装部署MongoDB数据库 - 无公网IP远程连接「内网穿透」 前言 …

Qt中QDebug的使用

QDebug类为调试信息(debugging information)提供输出流。它的声明在<QDebug>中&#xff0c;实现在Core模块中。将调试或跟踪信息(debugging or tracing information)写出到device, file, string or console时都会使用QDebug。 此类的成员函数参考&#xff1a;https://doc…

C语言—指针的进阶

指针的进阶 字符指针指针数组数组指针数组指针的定义区分&数组名以及数组名数组指针的使用 数组参数、指针参数一维数组传参二维数组传参一级指针传参二级指针传参 函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组相关笔试题和面试题指针笔试题 指针的相关概…

C++ Boost.Reflection库(现在是Boost.PFR)的下载、安装、使用详细教程

这里写目录标题 一、Boost.Reflection简介二、Boost.Reflection&#xff08;现在是Boost.PFR&#xff09;库的下载和安装2.1、下载编译Boost2.2、使用Boost.Reflection&#xff08;现在是Boost.PFR&#xff09; 小结 一、Boost.Reflection简介 ​ Boost.Reflection&#xff0…

【C++】-关于类和对象的默认成员函数(中)-构造函数和析构函数

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 ❤️‍&#x1fa79;作者宣言&#xff1a;认真写好每一篇博客 &#x1f4a8;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点…

Cadence(2):向导制作PCB封装

前提&#xff1a;软件版本 焊盘设计 &#xff1a;Pad Designer16.6PCB设计 &#xff1a;PCB Editor16.6 文章目录 LQFP64向导封装制作封装信息SMD焊盘制作添加焊盘库到PCB Editor新建工程LQFP64向导制作流程后续处理修改栅格大小。贴加1脚标识修改丝印线 总结 LQFP64向导封装制…

获取代码量(针对Pycharm IDEA)

Statistic 这是个一劳永逸&#xff0c;获取代码量的方法。 Beginning!&#xff08;安装到使用可能都不需要5分钟&#xff09; 附上Statistic的官网下载URL&#xff1a;Statistic - IntelliJ IDEs Plugin | Marketplacehttps://plugins.jetbrains.com/plugin/4509-statistic/v…

clion + opencv环境搭建

clion是一个jetbrains提供的c开发环境&#xff0c;和idea,pycharm等开发工具类似&#xff0c;界面有很多相似的地方。 clion内置了一个mingw的编译环境&#xff0c;自带了gcc,g等命令&#xff0c;安装完clion之后&#xff0c;我们新建项目&#xff0c;它会使用内置的mingw环境。…

在KylinV10上使用DTS进行mysql8迁移至DM8

前言&#xff1a; MySQL 到 DM 的移植主要有以下几个方面的工作&#xff1a; 1&#xff0e;分析待移植系统&#xff0c;确定移植对象。2&#xff0e;通过数据迁移工具 DTS 完成常规数据库对象及数据的迁移。3&#xff0e;通过人工完成 MSQL 的移植。4&#xff0e;移植完成后对移…

从设计到产品

从设计到产品 最近上的一些课的笔记&#xff0c;从 0 开始设计项目的角度去看产品。 设计系统 设计系统(design system) 不是 系统设计(system design)&#xff0c;前者更偏向于 UI/UX 设计部分&#xff0c;后者更偏向于实现部分。 个人觉得&#xff0c;前端开发与 UI/UX 设…

使用 ESP32 设计智能手表 – 第 1 部分制作表盘

相关设计资料下载ESP32 智能手表带心率、指南针设计资料(包含Arduino源码+原理图+Gerber+3D文件).zip 人们可以使用智能手表轻松快速地访问消息、警报、健康信息和其他高级信息。虽然智能手表作为独立设备在形式上是革命性的,但当与人们携带的其他设备(例如智能手机或平板…

作为一个大学生你应该知道的事情

作为一个大学生你应该知道的事情 大学生毕业去向 今天&#xff0c;我们不写技术&#xff0c;来谈一谈大学生的毕业现状&#xff1a; 以下内容为本人的一些观点和看法&#xff0c;仅限于沟通交流。 大学生毕业去向 大学生的毕业去向大致可以分为&#xff1a;就业、自由职业、慢…