手搓OS-day9

重量级难度,分配内存与回收(内核开始了),终于快一半了(折磨)

要求:

  • 边界对齐到\(2^i\)
  • 不够分配时返回NULL(0)
  • 拒绝超过16MiB的分配
  • 不必初始化内存,可以全赋值为零
  • 允许多处理器并行使用

C语言的面向对象

虽然很早之前老师就已经提过这个C语言的骚操作,嗯,还是有点难以接受

首先要介绍一个函数指针的概念,这个类似的概念实际上在Python里是接触过的,只是没有明确的提出来,当时令人感到很神奇(同时也感到很鸡肋)

函数指针

声明

1
return_type (*ptr)(parameter_type1,parameter_type2,……);

这样我们就声明了一个指向某个函数入口的一个指针,当然指针指向的函数需要初始化,一个小栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int add(int a,int b);

int main()
{
int (*ptr)(int,int);

ptr=add;

int result=ptr(10,20);
printf("result=%d\n",result);

return 0;
}

int add(int a,int b){
return a+b;
}

下面这个就是我们在Python中的常用方法了(回调函数):

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

#include <stdio.h>

typedef int (*Operation)(int, int);

int performOperation(int a, int b, Operation op) {
return op(a, b);
}

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
int result;

result = performOperation(10, 5, add);
printf("Add Result: %d\n", result);

result = performOperation(10, 5, subtract);
printf("Subtract Result: %d\n", result);

return 0;
}

C语言面向对象

虽然C++已经很好的做好了面向对象编程的,但是秉持着闲的蛋疼努力学习的精神,我们还是有必要了解如何用C语言实现面向对象编程的。面向对象三大特征:

  • 封装:这也叫特征(当然是),其重要思想在于不能随便更改对象的内容。

  • 继承:从而某个类继承相对应的属性和方法

  • 多态:其实就是子类的重写

封装对于C语言并不难实现,我们可以用结构体平替,那么怎么实现继承呢?鉴于接下来的文件结构比较抽象,请稍微记忆一下(写完发现并不复杂,没事了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// shape.h

#ifndef SHAPE_H
#define SHAPE_H

#include <stdint.h>

typedef struct {
int16_t x;
int16_t y;
}Shape;

void Shape_ctor(Shape * const me, int16_t x, int16_t y);
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
int16_t Shape_getX(Shape const * const me);
int16_t Shape_getY(Shape const * const me);


#endif /* SHAPE_H */

这里我们假设所有的接口函数都已经实现了(稍微好理解一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// rectangle.h

#ifndef RECT_H
#define RECT_H

#include "shape.h"

typedef struct{
Shape super;

uint16_t width;
uint16_t height;
}Rectangle;

// 构造函数
void Rectangle_ctor(Retangle *const me,int16_t x,int16_t y,uint16_t width,uint16_t height);

#endif /* RECT_H */

然后就是关于多态的实现这就很麻烦了

在C++中,如果一个父类中定义了虚函数,那么编译器就会在这个内存中开辟一块空间放置虚表,这张表里的每一个item都是一个函数指针,然后在父类的内存模型中放一个虚表指针,指向上面这个虚表。

  • 好消息:知道大概怎么回事了
  • 坏消息:虚函数是什么玩意来着

image-20240213212324586

哦,原来虚函数就是可重写的函数(还是Java事儿少),那没事了。来实现吧,首先重写一下shape类

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
// shape.h

#ifndef SHAPE_H
#define SHAPE_H

#include <stdint.h>

struct ShapeVtbl;

typedef struct {
struct ShapeVtbl const *vptr;
int16_t x;
int16_t y;
}Shape;

// 虚表定义
struct ShapeVtbl {
uint32_t (*area)(Shape const * const me); // 虚函数指针
};

// 接口函数
void Shape_ctor(Shape * const me, int16_t x, int16_t y);
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
int16_t Shape_getX(Shape const * const me);
int16_t Shape_getY(Shape const * const me);

static inline uint32_t Shape_area(Shape const * const me)
{
return (*me->vptr->area)(me);
}


#endif /* SHAPE_H */

ok,然后写一下这个rect.h的实现

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
// rect.c
#include "rect.h"
#include <stdio.h>
// 继承来的虚函数
static uint32_t Rectangle_area_(Shape const* const me);

// 构造函数
void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
uint16_t width, uint16_t height)
{
static struct ShapeVtbl const vtbl =
{
&Rectangle_area_
};
Shape_ctor(&me->super, x, y); // 调用基类的构造函数
me->super.vptr = &vtbl; // 重载 vptr
me->width = width;
me->height = height;
}

// 虚函数实现
static uint32_t Rectangle_area_(Shape const* const me){
Rectangle const* const me_ = (Rectangle const*)me;
return (uint32_t)me_->width *(uint32_t)me_->height;
}

记录

编译一个镜像要了半条命,缓缓

编译运行坑

  1. qemu模拟器现在包已经变了,应该下载的是:

    1
    sudo apt-get install qemu-system-x86
  2. 要在各种乱七八糟的Makefile里边手动添加一个变量AM_HEMO_,不过这个可能直接加在最外边的Makefile应该也是可以实现的

  3. 一些自己的写的头文件如果找不到就要用从根路径过来的全路径来引用,而且要是双引号

太感人了😭😭😭😭😭

printf实现

不说很难,至少很恶心代码量很大,尝试分析:

还是能分析出来的,回忆一下这个函数的用法:

1
2
printf("format",data, ...);
printf("string");

嗯,很显然我们需要一个和Python中argv类似性质的一个玩意来帮忙,然后要做的就是识别是否有后边的参数。其次我们还需要能够切分前面的字符串以识别其中的%d,%f等参数,并获取参数对应的argv中的参数。

果然,C语言中是有这种玩意的:<stdarg.h>,抄一个用法示例():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdarg.h>

int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址

va_list pArgs = NULL; //定义va_list类型的指针pArgs,用于存储参数地址

va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数

int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型

//若函数有多个可变参数,则依次调用va_arg宏获取各个变参

va_end(pArgs); //将指针pArgs置为无效,结束变参的获取

/* Code Block using variable arguments */

}

//可在头文件中声明函数为extern int VarArgFunc(int dwFixedArg, ...);,调用时用VarArgFunc(FixedArg, VarArg);

开编,注意到我们现在实际上只有一个putch()函数来输出单个字符

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
int printf(const char *fmt, ...)
{
if (fmt == NULL)
return -1;

// 定义返回字符串的数目
int ret_num = 0;
// 获取自由变量
va_list va_l;
va_start(va_l, fmt);
// 获取字符
char *pstr = (char *)fmt;
// 循环遍历
while (*pstr != '\0')
{
if (*pstr == '%')
{
pstr++;
switch (*pstr)
{
case 'd':
int var1 = va_arg(va_l, int);
ret_num++;
putch(var1);
break;
case 's':
char* var2 = va_arg(va_l, char *);
ret_num++;
putch(*var2);
break;
case '%':
putch('%');
ret_num++;
break;
default:
putch(' ');
ret_num++;
break;
}
}
putch(*pstr);
ret_num++;
}
return ret_num;
}

写了一个简陋的,但是显然不是很成功(思想对了,嗯,想看正确的可以看参考链接)

另外由于在内核编译的Makefile中有这样一句话:

1
CFLAGS  += -m64 -fPIC -mno -sse

因此我在编译内核时任何的浮点操作都是不被允许的。

kalloc、kfree实现

实现内容今天貌似是写不完了,简单记录一下思路,就是和前边实现字符串的一些操作是一致的,但是会涉及一些内存的管理问题,用结构体就能解决,区别主要是实现策略问题,然后就是要对齐,如何对齐呢,写个循环算好阶乘就行了。然后kfree的实现就比较简明了把维护的指针收回就行,剩下的明天续上。

参考链接

什么是函数指针?如何使用函数指针?-CSDN博客

C 语言实现面向对象编程_void rectangle_construct(rectangle_t* shape, const-CSDN博客

手把手教你实现printf函数(C语言方式)_printf实现-CSDN博客

内存分配不再神秘:深入剖析malloc函数实现原理与机制 - 知乎 (zhihu.com)