一、用户态printf
进入用户态之后,就不能再调用printk函数了。内核需要实现printf函数来打印,printf是底层调用了write这个系统调用。
1.1、文件描述符
在linux系统中,有三个文件描述符分别为标准输入,标准输出,标准错误输出,对应表示为0、1、2。打印默认是打印到标准输出,因此需要先定义标准输出:
typedef int32 fd_t;
typedef enum std_fd_t
{
stdin,
stdout,
stderr,
} std_fd_t;
如上所示,文件描述符是一个整型,标准输出,标准输出和标准错误输出通过枚举定义,分别是0, 1, 2。
1.2、write系统调用
printf需要通过write系统调用实现。write系统调用需要传入三个参数,因此还应该实现下三个参数的系统调用:
int32 write(fd_t fd, char *buf, u32 len)
{
return _syscall3(SYS_NR_WRITE, fd, (u32)buf, len);
}
_syscall3为三个参数的系统调用,我们这里就不再粘贴代码,通过内联汇编实现,和1个参数、2个参数的系统调用类似。当执行系统调用后,将会调用系统调用的中断函数,这是会中断函数是 sys_write
int32 sys_write(fd_t fd, char *buf, u32 len)
{
if (fd == stdout || fd == stderr)
{
return console_write(buf, len);
}
// todo
panic("write!!!!");
return 0;
}
sys_write 如果写入时标准输出或标准错误输出,就调用console_write打印到控制台,如果部署标准输出就panic,目前我们还未实现文件输出。 当然这是省略了write这个系统调用的初始化过程。最后我们就可以实现printf函数了:
static char buf[1024];
int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
i = vsprintf(buf, fmt, args);
va_end(args);
write(stdout, buf, i);
return i;
}
如上所示,printf和prink一样支持可变参数。获取可变参数列表后,调用write系统调用。
二、打印测试
在晚上printf函数的实现以后,就可以在用户态进行打印了。测试代码如下:
static void real_init_thread()
{
u32 counter = 0;
char ch;
while (true)
{
printf("task is in user mode %d\n", counter++);
}
}
运行该代码,就会在屏幕上持续打印。我们这里再详细说明下内核进入用户态以及系统调用进入内核态,调用结束再回到用户态的过程。
- 内核在进行初始化过程中,执行到进程初始化task_init,在进行init进程初始化时调用了task_to_user_mode(real_init_thread)
- task_to_user_mode中获取当前进程,即内核进程,修改该进程的栈内容,设置选择子为用户态选择子。申请一页内存并设置栈的esp指向这页内存,eip指向real_init_thread函数
- 直接执行中断返回函数,(此处只是模拟中断),返回后进入real_init_threa函数,此时已经进入用户态。
- 在内核态中,cs的值为23,即cpu的rpl被设置为3,运行在特权级3上。
- 在用户态中,执行printf函数。执行了write系统调用,CPU会自动检测当前的特权级属于用户态,进行特权级提升,栈自动从用户栈跳转到了内核栈,这是cpu硬件完成的,硬件自动 从 TSS(Task State Segment) 中加载 SS0 和 ESP0(内核栈指针),替换当前的用户栈 SS:ESP (SS0 和 ESP0在TSS初始化时就已经指定了)
- 进入内核态之后执行中断函数在屏幕上打印内容,中断结束恢复栈,回到用户态。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付
