Changchun Master Li

[RPi bring up] 树莓派实现两个进程交替打印ab

2024-02-24

My earliest test program was written to use one thread to write the letter A to the screen. The other thread wrote the letter B.
—— just for fun

linus在屏幕上交替打印A和B,验证操作系统进程切换是否能正常工作

emperorOS进程管理

上节我们实现了emperorOS的中断框架,在此基础之上,可以实现一个简单的进程管理器,PCB(Process Control Block) 进程控制块,是描述进程的数据结构,emperorOS里,我们定义proc为

1
2
3
4
5
6
7
8
struct proc {
enum procstate state; // Process state
int pid; // Process ID
struct proc *parent;
struct context context;
char name[16]; // Process name (debugging)
uint32_t pgd; // page descriptor
};
  • state 进程状态
  • pid 进程号
  • parent 父进程号
  • context 上下文
  • name 进程名
  • pgd 页描述符(1MB section页)

emperorOS内核进程和用户进程

进程调度实现

有两种调度方式,协作式Cooperative和抢占式Preemptive。协作式操作系统只能等待进程主动停止运行释放cpu,抢占式操作系统是由操作系统调度进程的运行,系统可以剥夺进程运行的权利。抢占式内核是指系统支持内核抢占。emperorOS所有进程共用同一个内核栈,系统在处于内核态会关闭中断,不支持内核抢占。

kernel完成启动后,首先proc_init初始化系统的第一个进程,然后proc_schd开始调度进程。proc_schd遍历procs列表,找到运行状态的进程,更新curproc,更新user页表,trap_return储存当前cpu上下文到context_schd。最后进入curproc的cpu上下文curproc-context,从而进入用户进程。

在用户进程运行当中,当发生外部中断或者异常,cpu进入特权模式,trap_enter储存cpu上下文到curproc-context。然后,进行相应的异常处理或者系统调用。最后,schd回到context_schd上下文,再次回到proc_schd遍历procs列表。

Linux内核是抢占式内核,kernel代码是可重入的,函数使用的所有变量都保存在调用堆栈,锁的作用就是kernel中非抢占区域的标记。emperorOS是非抢占内核,内核代码可以一直运行到完成而不被中断,无需要求内核代码可重入,也无需为每一个进程分配内核栈。

如果自旋锁已经被其它线程获取,那么另一个线程在获取锁的时候将循环等待。自旋锁只能锁当前的核,单cpu并且内核不可抢占的时候,自旋锁退化为空操作。

虚拟内存管理

当代操作系统内存分配庞大而繁杂,精确而动态,大部分32位操作系统都拥有两级页表,每页的粒度为4kb。

emperorOS用户程序只占虚拟内存最低1MB的空间,因此每次切换到用户进程前,loaduvm要替换当前用户进程相应的页描述符pgd

emperorOS address space

emperorOS系统调用

要完成两个进程交替打印ab,我们至少要实现fork和exec两个系统调用,fork创建子进程,exec从文件中载入进程。

用户程序init

系统启动后,载入elf程序bin/init

1
2
3
4
5
6
7
8
9
10
11
12
.globl start
start:
ldr r0, =fn
ldr r1, =argv
swi #4

fn:
.string "0:bin/init"

.align 2 // 地址类型,需要对齐4字节
argv:
.word 0

创建一个子进程,父进程打印a,子进程打印b

用户程序printab

printab.S

1
2
3
4
5
6
7
8
9
10
11
.globl start
start:
swi #1
cmp r0, #0
beq 2f
1:
swi #2
b 1b
2:
swi #3
b 2b

也可以用C程序实现,exec函数来加载elf文件

printab.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void printa() {
asm ("swi #2\n\t");
}

void printb() {
asm ("swi #3\n\t");
}

int fork() {
int res = 0;
asm volatile (
"swi #1\n\t"
"mov %[reg], r0"
: [reg] "=r" (res)
:
: "r0"
);
return res;
}
int _start() {
int res = fork();
while (1) {
if (res) {
printa();
} else {
printb();
}
}
return 0;
}

不需要c启动初始化代码,加nostartfiles参数编译。得到elf文件

1
arm-none-eabi-gcc -g -O0 -nostartfiles  --specs=nosys.specs printab.c

svc vs swi

以前叫swi指令(software interrupt),后来叫svc指令(Supervisor call)

svc指令产生svc中断异常,在操作系统中一般用于请求特权操作或系统资源。

eabi vs oabi

arm架构在演化过程中浮点单元也在不断演进,由此对应不同的abi(Application Binary Interface) ,oabi、eabi、eabihf。oabi old abi是arm架构第一个abi。

eabi和oabi一个很大的区别就是系统调用

1
2
oabi    swi syscall_number
eabi r7=syscall_number; swi 0x0

emperorOS使用的老式oabi的方式

Calling SWIs dynamically from an application

arm开发者文档中有一篇有趣的文章,讲述如何在应用中动态调用swi

elf解析

Executable and Linkable Format

elf文件

elf有三种header

  • 一个elf header
  • 多个program header,描述操作系统运行时segment的布局
  • 多个section header,描述程序编译时section的布局

segment和section在elf文件中可以是重叠的,是可执行文件的两种视角,大道至简,elf文件简单,兼容各种指令集架构。

exec系统调用

1
make user/init.hex

把init.hex填入initcode数组来编译到kernel中,这是系统的0号进程,用exec来加载运行elf文件bin/init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void execve() {
...

char* mem = kalloc();

int i, off;
for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
res = f_lseek(&fil, off);
if (res) while (1);
res = f_read(&fil, &ph, sizeof(ph), &count);
if (res) while (1);
printf("elf: proghdr %d bytes loaded\n", count);

if (ph.type != ELF_PROG_LOAD) {
continue;
}

if (ph.memsz < ph.filesz) while (1);
res = f_lseek(&fil, ph.off);
if (res) while (1);
res = f_read(&fil, mem+ph.vaddr, ph.filesz, &count);
if (res) while (1);
printf("elf: segment %d bytes loaded\n", count);
}
uint32_t pgd = createuvm(0, 0, mem);

// Push argument strings, prepare user stack.
uint32_t sp = (uint32_t)mem+0x00100000;

#define MAXARG 128
uint32_t ustack[MAXARG + 1];

int argc = 0;
for (argc = 0; argv[argc]; argc++) {
if (argc >= MAXARG) while(1);
sp = (sp - (strlen(argv[argc]) + 1)) & (~3);
memmove((void *)sp, argv[argc], strlen(argv[argc]) + 1);
ustack[argc] = sp;
}
ustack[argc] = 0;

sp -= (argc + 1) * 4;
curproc->context.r[0] = argc;
curproc->context.r[1] = sp;
memmove((void *)sp, ustack, (argc + 1) * 4);

// Save program name for debugging.
char *last, *s;
for (last = s = path; *s; s++) {
if (*s == '/') {
last = s + 1;
}
}
safestrcpy(curproc->name, last, sizeof(curproc->name));

uint32_t oldpgd = curproc->pgd;
curproc->pgd = pgd;
curproc->context.r[15] = elf.entry;
curproc->context.r[14] = 0xdeadbeef; // fake return PC, never exit now
curproc->context.r[13] = sp & 0x000fffff;

// reset fp if compile with nostartfiles
// curproc->context.r[11] = curproc->context.r[13];

freeuvm(oldpgd);

// exec returns only if an error has occurred
//curproc->context.r[0] = -1;
}
  1. exec解析elf,遍历每一个program header,把ph.off至ph.filesz中的代码载入到虚拟地址ph.vaddr
  2. 构建页描述符pgd
  3. 构造用户栈,压栈
  4. 删除原页描述符,释放原用户内存

fork系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void
proc_fork(void)
{
struct proc *p;
// Allocate process.
for(p = procs; p < &procs[NPROC]; p++) {
if(p->state == UNUSED) {
break;
}
}

p->state = RUNNABLE;
p->pid = nextpid++;
p->parent = curproc;
p->context = curproc->context;
strncpy(p->name, curproc->name, 16);

p->pgd = copyuvm(curproc->pgd, kalloc());

// in the child, fork returns 0
p->context.r[0] = 0;

// in the parent, fork returns child pid
curproc->context.r[0] = p->pid;
}

fork复制当前进程创建子进程,从procs中找到空位置,复制pcb进程控制块,复制内存到新的页描述符,设置返回值。

内存硬件

内存屏障指令

(arm11需要通过设置cp15协处理器)

DMB Data Memory Barrier 保证执行顺序

DSB Data Synchronization Barrier 保证内存访问指令完成顺序

ISB Instruction Synchronization Barrier 最严格,flush流水线

内存对齐

C语言编译器会自动帮助内存对齐

汇编需要align n伪指令,对齐2的n次方

页表内存参数

emperorOS disable了核心的cache/tlb

代码在github

1
git clone -b syscall https://github.com/996refuse/emperorOS.git
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章