操作系统-用户进程
一、Makefile
这个 Makefile 要比之前的文件夹中的 Makefile 更加复杂,是因为之前的文件夹都是对操作系统特定部分的一个编译指导,所以基本上是实现的功能就是“对应的 C 文件和汇编文件编译成目标文件”这一个功能,最后合成一个整体。但是 user
的 Makefile 指导的是多个用户程序的编译,最后生成的是多个用户目标文件,同时还需要给每个用户文件装备上库目标文件。
首先先补充一下 makefile 的一些知识
自动化变量
$@
表示目标(target)文件,就是冒号前面的那个文件$^
表示所有的依赖文件,就是冒号后面的那一堆文件$<
表示第一个依赖文件,就是紧挨着冒号后面的一个文件$*
这个变量表示目标模式中%
及其之前的部分。如果目标是dir/a.foo.b
,并且目标的模式是a.%.b
,那么,$*
的值就是dir/a.foo
。
静态模式
就是带有 %
的那种 target 和 prereq。主要用于同时匹配多个,之前似乎介绍过了。这里只是强调,对于匹配,是用 target 里面的集合元素去匹配 prereq 中的元素,换句话说,是一个从上到下的结构,比如说
all: a.x b.x%.x: %.o #这里
a.o: a.cgcc -o $@ $<
b.o: b.cgcc -o $@ $<
c.o: c.cgcc -o $@ $<
对于用注释标注出来的地方,虽然看上去,有 a.o, b.o, c.o
三个文件符合 %.o
的匹配条件,但是请注意,真正匹配到的只有 a.o, b.o
这是因为 all
作为总目标,指定了只要 a.x, b.x
,那么 %.x
就是 a.x, b.x
。所以就只会匹配到 a.o, b.o
。
中间文件
似乎 make
有一种特性是不保存中间文件,正是因为这种特性,我们在 user
下才会找不到 .b.c
文件,也找不到很多 .o
文件。
然后就可以来看文件了(考虑到理解问题,我们从下往上看):
%.o: lib.h
这个只是在保证 user
下有 lib.h
这个文件,不然编译就会报错。
%.o: %.c$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<%.o: %.S$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
这两句比较正常,就是把“对应的 C 文件和汇编文件编译成目标文件”。
接下来需要先从上往下看,这是因为本质上 makefile
是一个“需求决定工作”,而不是“工作决定需求”的东西,所以本源方法是从上往下看。
USERLIB := printf.o \print.o \libos.o \fork.o \pgfault.o \syscall_lib.o \ipc.o \string.oall: tltest.x tltest.b fktest.x fktest.b pingpong.x pingpong.b idle.x \$(USERLIB) entry.o syscall_wrap.o
这里我们可以看到,我们的目标文件有很多,但是我们可以将其分成三类,一类是库目标文件,也就是前面变量定义的
USERLIB := printf.o \print.o \libos.o \fork.o \pgfault.o \syscall_lib.o \ipc.o \string.o
一类是普通包装文件(瞎起的名字),他们是每个程序链接必须用到的(库文件则不一定,虽然这里是一定的)
entry.o syscall_wrap.o
最后是我们真的需要编译成成果的东西,即下面这个三个
tltest.x tltest.b fktest.x fktest.b pingpong.x pingpong.b idle.x
到最后我们用到的东西(也就是链接到整体的项目中的)是 .x
文件,它是一个二进制文件,.b
同样是一个二进制文件。只不过 .b
文件更接近我们通常的理解,而 .x
文件只是一个字符数组的二进制化。
然后我们来看 .x
文件是怎样来的
%.x: %.b.c$(CC) $(CFLAGS) -c -o $@ $<
可以看出,他是由一个名字相对应的 .b.c
的 C 文件编译而来的,但是这个 C 文件又是从何而?
%.b.c: %.bchmod +x ./bintoc./bintoc $* $< > $@~ && mv -f $@~ $@
我们可以看到,是由 .b
文件经过一个叫做 bintoc
的工具转换而来,首先是第一句
chmod +x ./bintoc
这是在赋予 bintoc
一个可执行的权限,这个工具的功能就是把一个二进制文件(这里是 .b
)转换为一个字符数组,我们可以在 user
文件夹下输入如下命令
make tltest.b.c
就会显式的得到 tltest.b.c
文件(如果是直接 make
,.b.c
会被判定为中间文件,不会保留),其内容如下
unsigned char binary_user_tltest_start[] = {
0x7f, 0x45, 0x4c, 0x46, 0x1, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x2, 0x0, 0x8, 0x0, 0x0, 0x0, 0x1, 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x34,
0x0, 0x0, 0x68, 0x44, 0x0, 0x0, 0x10, 0x1, 0x0, 0x34, 0x0, 0x20, 0x0, 0x2, 0x0, 0x28,
0x0, 0xa, 0x0, 0x7, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x25, 0xa0, 0x0, 0x40, 0x15, 0xa0,
0x0, 0x40, 0x15, 0xa0, 0x0, 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x4,
0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x10, 0x0, 0x0, 0x40, 0x0, 0x0,
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x53, 0x1a, 0x0, 0x0, 0x53, 0x24, 0x0, 0x0, 0x0, 0x7,
0x0, 0x0, 0x10, ...
0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x70, 0x63, 0x5f, 0x63, 0x61, 0x6e, 0x5f,
0x73, 0x65, 0x6e, 0x64, 0x0, 0x70, 0x61, 0x67, 0x65, 0x73, 0x0, 0x0
};
unsigned int binary_user_tltest_size = 29051;
这就与宏契合上了
#define ENV_CREATE(x) \
{ \extern u_char binary_##x##_start[]; \extern u_int binary_##x##_size; \env_create(binary_##x##_start, \(u_int)binary_##x##_size); \
}
然后我们就利用这个工具进行如下操作
./bintoc $* $< > $@~ && mv -f $@~ $@
我们会把这个内容输入到一个以 .b.c~
结尾的临时文件中,然后再用 mv
指令将其存入正式的 .b.c
文件中,我不知道其深意。
那么 .b
文件又是如何来的呢?
%.b: entry.o syscall_wrap.o %.o $(USERLIB)echo ld $@$(LD) -o $@ $(LDFLAGS) -G 0 -static -n -nostdlib -T ./user.lds $^
这里强调一下,无论是 .b, .b.c, .x
那个文件,其实没有 entry.o, fork.o
之类的事情,所有的文件名,都是围绕这四个名字展开的
tltest fktest pingpong idle
然后看代码
%.b: entry.o syscall_wrap.o %.o $(USERLIB)$(LD) -o $@ $(LDFLAGS) -G 0 -static -n -nostdlib -T ./user.lds $^
这里做的其实是把上面编译好的东西链接起来,也就是给每个 tltest fktest pingpong idle
的东西链接上 entry.o syscall_wrap.o
和库目标文件。
至此,我们可以有一个大致的思路,我们首先编译出很多个目标文件,这其中有真正我们的程序目标文件,有的则是库文件,我们的目的是将库文件链接到每一个写好的程序上。这个时候会生成 .b
文件,这是可执行文件。但是因为此时我们没有文件系统,所以读不了它,所以我们需要将其转换成字符数组的形式,我们用这个工具bintoc
生成了 .b.c
的 C 文件,这个时候我们只需要将其重新编译成二进文件即可,即 .x
文件。
二、user.lds
这个链接脚本就很普通
OUTPUT_ARCH(mips)
ENTRY(_start)
SECTIONS
{. = 0x00400000;_text = .; /* Text and read-only data */.text : {*(.text)*(.fixup)*(.gnu.warning)}_etext = .; /* End of text section */.data : { /* Data */*(.data)*(.rodata)*(.rodata.*)*(.eh_frame)CONSTRUCTORS}_edata = .; /* End of data section */. = ALIGN(0x1000);__bss_start = .; /* BSS */.bss : {*(.bss)}/DISCARD/ : {*(.comment)*(.debug_*)}end = . ;}
只是指定了入口函数是 _start
(注意是用户进程的 _start
,在 entry.S
中)。
然后将所有的代码从低地址区开始链接
. = 0x00400000;
三、启动流程
这个 _start
还是很简单的,其简单的最主要原因就是很多事情都是由内核完成的,所以才会使这里这么简单,其中在 env_alloc
中有
e->env_tf.cp0_status = 0x1000100c;
e->env_tf.regs[29] = USTACKTOP;
可以看到这里设置了栈指针和 CP0_STATUS。于是当调度的时候,sp
已经是用户栈指针了。所以对于 _start
。做的事情就是从用户栈上给 libmain
传入两个参数,就是大名鼎鼎的 argc, argv
。
.text.globl _start
_start:lw a0, 0(sp)lw a1, 4(sp)
nopjal libmainnop
对于libmain
void exit(void)
{syscall_env_destroy(0);
}struct Env *env;void libmain(int argc, char **argv)
{env = 0; int envid;envid = syscall_getenvid();envid = ENVX(envid);env = &envs[envid];umain(argc, argv);exit();
}
首先是先定义了一个 env
全局指针,这个指针指向了这个进程对应的进程控制块。还是挺有意思的。
struct Env *env;
envid = ENVX(envid);
env = &envs[envid];
可以看到 libmain
主要还是起一个包装的作用,在正式开始前获得进程控制块,然后进行了一个参数传参。在正式结束后把进程控制块在操作系统中注销。
void exit(void)
{syscall_env_destroy(0);
}
最后举一个 umain
的例子
#include "lib.h"void umain()
{while (1) {writef("IDLE!");}
}
可以看到在这里的时候,就很像我们平时写的程序了。到了这里,我们可以说,我们终于对于用户完成了封装。用户只需要引用用户可知的头文件,就可以实现相应的功能,而不需要了解操作系统的实现细节。
四、库函数
这里说明,这里的库函数包括我在 makefile
这一节中提出的库函数和普通包装函数,放在这里一并记录了。
4.1 entry.S
__asm_pgfault_handler
.globl __pgfault_handler
__pgfault_handler:
.word 0.set noreorder
.text
.globl __asm_pgfault_handler
__asm_pgfault_handler:
noplw a0, TF_BADVADDR(sp)lw t1, __pgfault_handlerjalr t1noplw v1,TF_LO(sp)mtlo v1lw v0,TF_HI(sp)lw v1,TF_EPC(sp)mthi v0mtc0 v1,CP0_EPClw $31,TF_REG31(sp)lw $30,TF_REG30(sp)lw $28,TF_REG28(sp)lw $25,TF_REG25(sp)lw $24,TF_REG24(sp)lw $23,TF_REG23(sp)lw $22,TF_REG22(sp)lw $21,TF_REG21(sp)lw $20,TF_REG20(sp)lw $19,TF_REG19(sp)lw $18,TF_REG18(sp)lw $17,TF_REG17(sp)lw $16,TF_REG16(sp)lw $15,TF_REG15(sp)lw $14,TF_REG14(sp)lw $13,TF_REG13(sp)lw $12,TF_REG12(sp)lw $11,TF_REG11(sp)lw $10,TF_REG10(sp)lw $9,TF_REG9(sp)lw $8,TF_REG8(sp)lw $7,TF_REG7(sp)lw $6,TF_REG6(sp)lw $5,TF_REG5(sp)lw $4,TF_REG4(sp)lw $3,TF_REG3(sp)lw $2,TF_REG2(sp)lw $1,TF_REG1(sp)lw k0,TF_EPC(sp) jr k0lw sp,TF_REG29(sp)
这个函数在“异常处理流”中讲过了,主要是跳转到函数指针 __pgfault_handler
指向的异常处理函数主体(pgfault),然后进行现场的恢复。
在这个文件中还有与自映射有关的两个变量
.globl vpt
vpt:.word UVPT.globl vpd
vpd:.word (UVPT+(UVPT>>12)*4)
其实就类似与
unsigned int vpt = UVPT;
unsigned int vpd = (UVPT+(UVPT>>12)*4);
关于为啥不直接使用原来定义好的宏,我觉得是因为原来的宏是属于操作系统的,而在库函数的实现的时候尽量少的接触操作系统的细节,是很有必要的,因为这样可以提高可移植性。
4.2 fork.c
首先先吐槽一下,这个函数分类真的是烂透了,为什么 fork.c
中要有 user_bcopy()
,真的理解不了啊。
user_bcopy
实现就类似与 bcopy
void user_bcopy(const void *src, void *dst, size_t len)
{void *max;max = dst + len;// copy machine words while possibleif (((int)src % 4 == 0) && ((int)dst % 4 == 0)){while (dst + 3 < max){*(int *)dst = *(int *)src;dst += 4;src += 4;}}// finish remaining 0-3 byteswhile (dst < max){*(char *)dst = *(char *)src;dst += 1;src += 1;}
}
user_bzero
与 user_bcopy
类似
void user_bzero(void *v, u_int n)
{char *p;int m;p = v;m = n;while (--m >= 0){*p++ = 0;}
}
pgfault
这个函数实现的是根据虚拟地址 va
为其分配一个物理页面,而且这个新的物理页面要有一些内容。
最有意思的是,这个 va
之前是对应了一个物理页面的(这是一个子进程函数,所以之前是和父进程共享这个页面)。那么我们要实现的,似乎是让一个 va
对应两个物理页面。显然是不合理的。所以严谨地阐述这个函数的功能,是将 va
对应到新的物理页面,并将原来的物理页面映射关系去掉。这个新的物理页面的内容跟原来的物理页面内容一致。
static void pgfault(u_int va)
{u_int *tmp = USTACKTOP;// writef("fork.c:pgfault():\t va:%x\n",va);u_long perm = ((Pte *)(*vpt))[VPN(va)] & 0xfff;if ((perm & PTE_COW) == 0){user_panic("pgfault err: COW not found");}perm -= PTE_COW;// map the new page at a temporary placesyscall_mem_alloc(0, tmp, perm);// copy the contentuser_bcopy(ROUNDDOWN(va, BY2PG), tmp, BY2PG);// map the page on the appropriate placesyscall_mem_map(0, tmp, 0, va, perm);// unmap the temporary placesyscall_mem_unmap(0, tmp);
}
duppage
这个函数用于根据父进程的映射关系,去复制子进程的映射关系,pn
是虚拟页面号的意思。复制最困难的是对于权限位的考量,其实就是对于 COW
的设置。如果一个页面,他不是只读的(说明有写的可能),而且也没有明确说是可以共享的(只共享写),那么就是应该增设 PTE_COW
位,这种增设是对于父子进程都要设置的。所以尽管这里有两个map
,但是第一个 map
是用于子进程建立页面映射,而第二个是用于修改父进程的映射权限。
static void duppage(u_int envid, u_int pn)
{// addr is the va we need to processu_int addr = pn << PGSHIFT;// *vpt + pn is the adress of page_table_entry which is corresponded to the vau_int perm = ((Pte *)(*vpt))[pn] & 0xfff;// if the page can be write and is not shared, so the page need to be COW and map twiceint flag = 0;if ((perm & PTE_R) && !(perm & PTE_LIBRARY)){perm |= PTE_COW;flag = 1;}syscall_mem_map(0, addr, envid, addr, perm);if (flag){syscall_mem_map(0, addr, 0, addr, perm);}// user_panic("duppage not implemented");
}
fork
这个函数用于产生一个子进程,并且设置其状态和各种配置。这里需要强调的一个有趣的点是,fork
本身并不是系统调用函数,他是由一系列系统调用函数组成的一个用户函数。
int fork(void)
{u_int newenvid;extern struct Env *envs;extern struct Env *env;u_int i;// The parent installs pgfault using set_pgfault_handlerset_pgfault_handler(pgfault);// alloc a new allocnewenvid = syscall_env_alloc();if (newenvid == 0){env = envs + ENVX(syscall_getenvid());return 0;}for (i = 0; i < VPN(USTACKTOP); ++i){if (((*vpd)[i >> 10] & PTE_V) && ((*vpt)[i] & PTE_V)){duppage(newenvid, i);}}syscall_mem_alloc(newenvid, UXSTACKTOP - BY2PG, PTE_V | PTE_R);syscall_set_pgfault_handler(newenvid, __asm_pgfault_handler, UXSTACKTOP);syscall_set_env_status(newenvid, ENV_RUNNABLE);return newenvid;
}
首先我们先进行了一个父进程的配置,我们用这个函数为父进程分配了处理 COW
的时候的栈,还指定了处理 pgfault
异常的函数。至于为啥不一早就分配好了呢?我觉得是因为不是每个进程都需要用到这个栈,所以为了避免页面的浪费,就没有改成了用函数手动配置,而不是默认配置。
set_pgfault_handler(pgfault);
然后我们利用系统调用创造一个进程
newenvid = syscall_env_alloc();
我们先看子进程,它会被时钟中断调度(先别管咋调度的),那么就会从内存控制块里恢复现场,那么此时被恢复的 v0
就是 0
,返回的 PC 就是 syscall_env_alloc
所导致的 syscall
的下一条,也就是 msyscall
中的这条
LEAF(msyscall)syscallnop // 这条jr ranop
END(msyscall)
那么再次返回的时候 syscall_env_alloc
的返回值就变成了 0。然后就会进入下面这个分支判断
if (newenvid == 0)
{env = envs + ENVX(syscall_getenvid());return 0;
}
env
是对于用户进程的一个全局变量,他表示正在运行的进程块,一般会在 start
到 main
之间设置,但是以为 fork
出的子进程没有设置这个,所以需要在这里设置,然后就可以结束 fork
了,返回值是 0
。
但是对于父进程来说,对于子进程的修改还没有结束,他还需要将虚拟环境完全的复制给子进程,也就是下面的语句。这里的 i
是虚页号,我们可以用 (*vpd)[i >> 10]
的找出这个虚页号对应的一级页表项,用 (*vpt)[i]
找出二级页表项,为什么可以这样呢?
for (i = 0; i < VPN(USTACKTOP); ++i)
{if (((*vpd)[i >> 10] & PTE_V) && ((*vpt)[i] & PTE_V)){duppage(newenvid, i);}
}
首先我们需要弄清 extern
的用法,这似乎是理解的最难点,对于一个在 file1
中定义的全局变量 a
int a = 1; // 假设地址 &a = 0x8000_5000
那么在文件 file2
的时候需要引入这个变量,那么可以有两种写法(虽然正常人只会用第一种)
extern int a;
extern int a[];
但是两者的结果是不同的,如果我们打印第一个变量 a
,那么会出现 1
,如果打印第二个变量(我都感觉这是个指针常量了),那么就会出现 a = 0x80005000
。我不知道为啥是这样的,但是确实是这样的。
然后我们来看一下 vpt
和 vpd
的定义,在 user
文件夹下的 entry.S
下
.globl vpt
vpt:.word UVPT.globl vpd
vpd:.word (UVPT+(UVPT>>12)*4)
可以看到是一个自映射的标准写法,相关的宏就是我们在 mmu.h
中定义的,而且我们在进程创建之初,就完成了这个设置,
e->env_pgdir[PDX(UVPT)] = e->env_cr3 | PTE_V;
但是最让人困惑的莫过于教程中“指针的指针”这一说法,我个人觉得直接认为他是错误的就好了。因为在引入的时候,我们用的是这种方法
extern volatile Pte *vpt[];
extern volatile Pde *vpd[];
按照 c 的语法,vpt
应该是一个指针数组,但是这个操作 (*vpt)[i]
如果需要先按照数组方式理解,然后再按照指针方式理解,那么就会变成这样 *(vpt[0] + i)
或者直观一些 vpt[0][i]
。这都是无厘头的,因为类似于我们声明了一个指针数组,但是只用它的第一个元素当指针,为啥我们不直接声明一个指针 Pte* vpt
。这是个未解之谜,我与叶哥哥讨论,叶哥哥也认为如果写成
u_int *vpt = (u_int*) UVPT;
// use vpt[i]
会很好看,鬼知道他为啥写成这样。
但是既然写了,就要从语法上解释通,对于 (*vpt)
操作,结合上面介绍的 extern
知识,可以知道,vpt
的值不再是 0x7fc0 0000
了,而是 vpt
的地址(恶心)。然后 (*vpt)
的值才是 0x7fc0 0000
。所以再结合 (*vpt) + VPN
,知道这是在计算二级页表项的虚拟地址,然后取地址,就可以得到二级页表项 *((*vpt) + VPN) = (*vpt)[VPN]
。
在有了这些知识打底的基础上,我们就可以看到底要干啥了,我们遍历了所有的二级页表项和一级页表项,如果他是有效的,那么就要给子进程复制他,人物交给了 duppage
。
for (i = 0; i < VPN(USTACKTOP); ++i)
{if (((*vpd)[i >> 10] & PTE_V) && ((*vpt)[i] & PTE_V)){duppage(newenvid, i);}
}
然后我们需要设置一些子进程的对于 COW
的设置
syscall_mem_alloc(newenvid, UXSTACKTOP - BY2PG, PTE_V | PTE_R);
syscall_set_pgfault_handler(newenvid, __asm_pgfault_handler, UXSTACKTOP);
上面两句话的作用其实跟父进程的 set_pgfault_handler(pgfault)
作用一样,就是没有设置 pgout
因为之前似乎已经设置了。
最后我们需要让子进程进入调度序列,就是底下这个函数实现的
syscall_set_env_status(newenvid, ENV_RUNNABLE);
4.3 ipc.c
这个文件中记录着与进程通信有关的函数。
ipc_send
void ipc_send(u_int whom, u_int val, u_int srcva, u_int perm)
{int r;while ((r = syscall_ipc_can_send(whom, val, srcva, perm)) == -E_IPC_NOT_RECV) {syscall_yield();}if (r == 0) {return;}user_panic("error in ipc_send: %d", r);
}
这个函数主要是一个忙等的实现,通过不断的尝试
syscall_ipc_can_send(whom, val, srcva, perm)
这个函数就是一个尝试并获得的过程。如果没有获得成功,就会被 yield
。
ipc_recv
这个函数就没有忙等,是因为其系统调用的实现中实现了 wait-notify
机制。
u_int ipc_recv(u_int *whom, u_int dstva, u_int *perm)
{syscall_ipc_recv(dstva);if (whom) {*whom = env->env_ipc_from;}if (perm) {*perm = env->env_ipc_perm;}return env->env_ipc_value;
}
4.4 lib.h
这个头文件里包括了所有的库函数声明(丑爆了)。
4.5 pgfault.c
set_pgfault_handler
这个函数看着很奇怪,是因为把函数调用写到了条件里,其实比较好看懂的写法应该是这样(不严谨)
void set_pgfault_handler(void (*fn)(u_int va))
{if (__pgfault_handler == 0) {syscall_mem_alloc(0, UXSTACKTOP - BY2PG, PTE_V | PTE_R);syscall_set_pgfault_handler(0, __asm_pgfault_handler, UXSTACKTOP);}// Save handler pointer for assembly to call.__pgfault_handler = fn;
}
也就是说,当一个进程第一次调用 fork
的时候(对应的就是 __pgfault_handler == 0
)。那么这个函数就会为他分配出一个用来处理 pgfault
异常的栈,而且设置他处理这种异常的函数是 __asm_pgfault_handler
。
每次这个函数都会将 __pgfault_handler
设置成 fn
,在 fork
中是传入参数是 pgfault
。
void set_pgfault_handler(void (*fn)(u_int va))
{if (__pgfault_handler == 0) {// Your code here:// map one page of exception stack with top at UXSTACKTOP// register assembly handler and stack with operating systemif (syscall_mem_alloc(0, UXSTACKTOP - BY2PG, PTE_V | PTE_R) < 0 ||syscall_set_pgfault_handler(0, __asm_pgfault_handler, UXSTACKTOP) < 0) {writef("cannot set pgfault handler\n");return;}// panic("set_pgfault_handler not implemented");}// Save handler pointer for assembly to call.__pgfault_handler = fn;
}
4.6 print.c
就不记录了,因为跟内核态的 print.c
一模一样。
4.7 printf.c
writef
writef
对应的就是用户态的 printf
。这个名字烂爆了,因为 write
有写的意思,很容易跟人造成“写”的错觉,而不是“打印”。
static void user_myoutput(void *arg, const char *s, int l)
{int i;// special termination callif ((l == 1) && (s[0] == '\0')){return;}for (i = 0; i < l; i++){syscall_putchar(s[i]);if (s[i] == '\n'){syscall_putchar('\n');}}
}void writef(char *fmt, ...)
{va_list ap;va_start(ap, fmt);user_lp_Print(user_myoutput, 0, fmt, ap);va_end(ap);
}
_user_panic
跟 panic
的实现完全相同。
#define user_panic(...) _user_panic(__FILE__, __LINE__, __VA_ARGS__)void _user_panic(const char *file, int line, const char *fmt, ...)
{va_list ap;va_start(ap, fmt);writef("panic at %s:%d: ", file, line);user_lp_Print(user_myoutput, 0, (char *)fmt, ap);writef("\n");va_end(ap);for (;;);
}
4.8 string.c
这里面定义了一些简单的字符串处理函数,但是在此时还没用到,猜测会在文件系统中使用。
strlen
求字符串长度,这里唯一一个亮点是 const char *s
保证了 s
指向的字符串不会被更改。
int strlen(const char *s)
{int n;for (n = 0; *s; s++){n++;}return n;
}
strcpy
字符串拷贝函数。
char *strcpy(char *dst, const char *src)
{char *ret;ret = dst;while ((*dst++ = *src++) != 0);return ret;
}
strchr
这个函数用于找寻字符串中有无特定的字符,如果有,就返回字符串中指向这个字符首次出现位置的指针。
const char *strchr(const char *s, char c)
{for (; *s; s++)if (*s == c){return s;}return 0;
}
strcmp
用于进行字符串之间的比较,比较方法就是普通的字典序比较。
int strcmp(const char *p, const char *q)
{while (*p && *p == *q){p++, q++;}if ((u_int)*p < (u_int)*q){return -1;}if ((u_int)*p > (u_int)*q){return 1;}return 0;
}
memcpy
复制一段东西。有一说一,我不知道在有了 user_bcopy
后,为啥还要用这个(还慢)
void *memcpy(void *destaddr, void const *srcaddr, u_int len)
{char *dest = destaddr;char const *src = srcaddr;while (len-- > 0){*dest++ = *src++;}return destaddr;
}
4.9 syscall_lib.c
就是一大堆函数调用 msyscall
,一般格式就这样(会有参数占位符)
#include <unistd.h>void syscall_putchar(char ch)
{msyscall(SYS_putchar, (int)ch, 0, 0, 0, 0);
}
4.10 syscall_wrap.S
msyscall
LEAF(msyscall)syscallnopjr ranop
END(msyscall)
相关文章:

操作系统-用户进程
一、Makefile 这个 Makefile 要比之前的文件夹中的 Makefile 更加复杂,是因为之前的文件夹都是对操作系统特定部分的一个编译指导,所以基本上是实现的功能就是“对应的 C 文件和汇编文件编译成目标文件”这一个功能,最后合成一个整体。但是 …...

小驰私房菜_07_camx EIS使能
#小驰私房菜# #Qcom Cax# 本篇文章分下面几点展开: 1) camxoverridesettings.txt 中如何设置打开eis开关? 2)app打开eis,需要设置哪些request? 3) eisv2.0、eisv3.0分别是什么时候采用? 4)相关日志分析,日志上如何确认eis已经使能? 一、 camxoverridesettings.txt …...

互联网快速发展,孕育着新技术、新模式的全新时代正在到来
除了新时代的红利之外,在马云的回归之下,我更多地看到的是,人们信心的回归。这样一种回归,并不仅仅只是局限于企业家本身,纵然是对于普通民众来讲,同样是一种信心的回归。时下,经济复苏的号角开…...

【VUE】1、安装node.js
1、什么是 node.js 官方:Node.js is an open-source, cross-platform JavaScript runtime environment. 翻译:Node.js 是一个开源、跨平台的 JavaScript 运行时环境。 Node.js发布于2009年5月,由Ryan Dahl开发,是一个基于Chrome…...

一文弄懂window.print()打印
一文弄懂window.print 打印前言window.print() 默认效果缺陷一、打印样式二、打印指定区域内容1. 对容器进行打印2. 对容器内的部分内容进行打印3. 监听打印前后事件4. iframe三、强行插入分页四、打印设置五、最佳实践(React)1. 背景:2. 思路…...

卷麻了,00后测试用例写的比我还好,简直无地自容.....
前言 作为一个测试新人,刚开始接触测试,对于怎么写测试用例很头疼,无法接触需求,只能根据站在用户的角度去做测试,但是这样情况会导致不能全方位的测试APP,这种情况就需要一份测试用例了,但是不…...

mysql性能优化之explain分析执行计划
前言 在实际工作中,如果已经定位到某些具体的sql需要进行explain分析进而优化,可以直接使用explainsql来分析其执行计划;如果还不能确定是哪些具体的sql语句需要进行explain分析进而优化,那么我们可以首先要定位哪些sql查询慢&…...

IDEA修改关键字和注释颜色
IDEA修改关键字和注释颜色 目录IDEA修改关键字和注释颜色1.修改关键字的默认颜色2.修改注释的默认颜色2.1 修改单行注释的颜色2.2 修改多行注释的颜色2.3 修改文档注释的颜色很多小白在刚刚使用IDEA的时候还不是很熟练 本文主要给大家提供一些使用的小技巧,希望能帮…...

数据库总结/个人总结
目录数据库数据和信息Data数据数据库数据库管理系统总结常见的数据库管理系统关系型数据库连接查询交叉连接、笛卡尔积内连接左连接右连接嵌套查询Jar在Java项目中使用.jar文件JDBC核心接口单表查询SQL注入简化JDBC视图View创建视图使用视图删除视图事务transaction事务的特性A…...

【Maven】开发自己的starter依赖
【Maven】开发自己的starter依赖 文章目录【Maven】开发自己的starter依赖1. 准备工作1.1 创建一个项目1.2 修改pom文件1.3 修改项目结构2. 动手实现2.1 创建客户端类2.2 创建配置类2.3 配置路径2.4 下载到本地仓库3. 测试1. 准备工作 1.1 创建一个项目 打开idea,…...

JVM与Java体系
JVM体系跟着尚硅谷的康师傅学习 JVM内存与垃圾回收概述 除了大部分的Java开发 人员,除了会在项目中使用到与Java平台相关的框架,与API,对于Java的虚拟机了解甚少。但是也需要我们知道如何处理OOM,SOF异常,除了…...

【C++笔试强训】第十二天
选择题 解析:引用:引用是对象的别名,并没有开辟属于自己的空间,两者同用一块内存,引用值改变也会引起引用对象值的改变; 引用在声明的时候必须要初始化,而指针不用,指针可以为空指针…...

C# | 使用DataGridView展示JSON数组
C# | 使用DataGridView展示JSON数组 文章目录C# | 使用DataGridView展示JSON数组前言实现原理实现过程完整源码前言 你想展示一个复杂的JSON数组数据吗?但是你却不知道该如何展示它,是吗?没问题,因为本文就是为解决这个问题而生的…...

Python入门到高级【第四章】
预计更新第一章. Python 简介 Python 简介和历史Python 特点和优势安装 Python 第二章. 变量和数据类型 变量和标识符基本数据类型:数字、字符串、布尔值等字符串操作列表、元组和字典 第三章. 控制语句和函数 分支结构:if/else 语句循环结构&#…...

【ChatGPT】ChatGPT 能否取代程序员?
Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员,2024届电子信息研究生 目录 前言: ChatGPT 的优势 自然语言的生成 文本自动生成 建立了更人性化的人机交互 ChatGPT 的局限性 算法的解释能力较差 程序的可实现性较差 缺乏优化和质量控制 程序员相较于 …...

英飞凌Tricore问题排查01_Det/Reset/Trap排查宝典
目录 1.概述2. 排查方法总览(流程图)3. 进Det排查方法4. 进Reset/Trap排查4.1 通过ErrorHook/ProtectionHook排查4.2. 通过BTV寄存器排查Trap方法4.3 借助英飞凌寄存器排查4.3.1 借助Reset状态寄存器4.3.2 SMU触发的复位4.3.3 CPU触发的复位1.概述 大家在软件开发过程中,可…...

第六章 共享模型之 无锁
JUC并发编程系列文章 http://t.csdn.cn/UgzQi 文章目录JUC并发编程系列文章前言一、问题的引出如何保证取款方法的线程安全解决方案一、使用synchronized锁住临界区代码解决方案二、无锁(AtomicInteger 原子整数类)二、CAS 与 volatileAtomicInteger . compareAndSet( ) 方法的…...

2023Q2押题,华为OD机试用Python实现 -【机智的外卖员】
最近更新的博客 华为 od 2023 | 什么是华为 od,od 薪资待遇,od 机试题清单华为 OD 机试真题大全,用 Python 解华为机试题 | 机试宝典【华为 OD 机试】全流程解析+经验分享,题型分享,防作弊指南华为 od 机试,独家整理 已参加机试人员的实战技巧本篇题解:机智的外卖员 题目…...

【华为OD机试真题】密室逃生游戏(javapython)
密室逃生游戏 题目 小强增在参加《密室逃生》游戏,当前关卡要求找到符合给定 密码 K(升序的不重复小写字母组 成) 的箱子, 并给出箱子编号,箱子编号为 1~N 。 每个箱子中都有一个 字符串 s ,字符串由大写字母、小写字母、数字、标点符号、空格组成, 需要在这些字符串中…...

[golang gin框架] 17.Gin 商城项目-商品分类模块, 商品类型模块,商品类型属性模块功能操作
一.商品分类的增、删、改、查,以及商品分类的自关联1.界面展示以及操作说明列表商品分类列表展示说明:(1).增加商品分类按钮(2).商品分类,以及子分类相关数据列表展示(3).排序,状态,修改,删除操作处理 新增编辑删除修改状态,排序2.创建商品分类模型在controllers/admin下创建Go…...

Redis安装-使用包管理安装Redis
这种在Linux上使用apt-get包管理器安装Redis的方式称为“包管理安装”。这种安装方式使用操作系统的官方软件库来获取和安装软件包,可以自动处理软件包的依赖关系,并确保软件包与系统其他部分兼容。这是一种安全、可靠且方便的安装方式,适用于…...

HTML属性的概念和使用
通过前面的学习,我们已经对 HTML标签 有了简单的认识,知道可以在标签中可以添加一些属性,这些属性包含了标签的额外信息,例如: href 属性可以为 <a> 标签提供链接地址;src 属性可以为 <img> 标…...

ChatGPT基础知识系列之一文说透ChatGPT
ChatGPT基础知识系列之一文说透ChatGPT OpenAI近期发布聊天机器人模型ChatGPT,迅速出圈全网。它以对话方式进行交互。以更贴近人的对话方式与使用者互动,可以回答问题、承认错误、挑战不正确的前提、拒绝不适当的请求。高质量的回答、上瘾式的交互体验,圈内外都纷纷惊呼。 …...

‘go install‘ requires a version when current directory is not in a module
背景 安装好环境之后,跑个helloworld看看 目录结构 workspacepathsrchellohelloworld.go代码: package mainimport "fmt"func main() { fmt.Println("Hello World") }1.使用 go run 命令 - 在命令提示符旁,输入 go …...

蓝桥杯嵌入式第十三届(第二套客观题)
文章目录 前言一、题目1二、题目2三、题目3四、题目4五、题目5六、题目6七、题目7八、题目8九、题目9十、题目10总结前言 本篇文章继续讲解客观题。 一、题目1 这个其实属于送分题,了解嵌入式或者以后想要入行嵌入式的同学应该都对嵌入式特点有所了解。 A. 采用专用微控制…...

FFmpeg进阶:各种输入输出设备
文章目录查看设备列表输入设备介绍输出设备介绍查看设备列表 我们可以通过ffmpeg自带的工具查看系统支持的设备列表信息, 对应的指令如下所示: ffmpeg -devices输入设备介绍 通过配置ffmpeg的输入设备,我们可以访问系统中的某个多媒体设备的数据。下面详细介绍一下各个系统中…...

使用Shell笔记总结
一、变量 1、定义变量不加$符号,使用变量要用$;等号两边不能直接接空格符;通常大写字符为系统默认变量,自行设定变量可以使用小写字符。 2、双引号内的特殊字符如 $ 等,可以保有其符号代表的特性,即可以有…...

反常积分的审敛法
目录 无穷先的反常积分的审敛法 定理1:比较判别法 例题: 比较判别法的极限形式: 例题: 定理3:绝对收敛准则 例题: 无界函数的反常积分收敛法 例题: 无穷先的反常积分的审敛法 定理1&#x…...

python实战应用讲解-【numpy专题篇】numpy常见函数使用示例(十三)(附python示例代码)
目录 Python numpy.ma.mask_or()函数 Python numpy.ma.notmasked_contiguous函数 Python numpy.ma.notmasked_edges()函数 Python numpy.ma.where()函数 Python Numpy MaskedArray.all()函数 Python Numpy MaskedArray.anom()函数 Python Numpy MaskedArray.any()函数 …...

Java设计模式(十九)—— 桥接模式
桥接模式定义如下:将抽象部分与它的实现部分分离,使它们都可以独立地变化。 适合桥接模式的情景如下: 不希望抽象和某些重要的实现代码是绑定关系,可运行时动态决定抽象和实现者都可以继承的方式独立的扩充,程序在运行…...