从JOS源码了解系统调用

本文深入浅出地介绍了系统调用的概念及其在JOS操作系统中的实现过程。从用户程序的视角出发,逐步剖析了如何通过一系列函数调用最终到达系统调用层面,并详细解读了关键函数的实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        首先何为系统调用?根据维基百科的解释:a system call is how a program requests a service from an operating system's kernel. 简意就是用户程序对操作系统内核服务的请求。我们知道用户程序所能做的事情很有限,比如我们不能自己写代码直接操作硬盘,不能自己写代码控制控制台的输入输出等等,那怎么办呢?办法就是请求操作系统帮我们完成,毕竟操作系统掌控着所有的计算资源,因此对它来说,控制硬盘,显示器啦肯定都是不在话下的。

       其实我们平时写的很多用户程序都用到系统调用,只不过由于一般编程语言都会把系统调用封装在标准库里面,所以我们没有发觉。因此系统调用很多时候对我们来说就是简单的调用函数。比如c语言中,我们最常用的pritf格式化输出函数就需要用到系统调用,毕竟将一些东西显示到显示器上还不是那么容易的~

      下面以JOS操作系统的源码来了解一下系统调用,以及他是怎么封装在函数当中的。


     首先假如我们编写了以下用户程序

// hello, world
#include <inc/lib.h>

void
main(int argc, char **argv)
{
	cprintf("hello, world!\n");
}


首先需要调用打印函数,看一下打印函数的实现

int
cprintf(const char *fmt, ...)
{
	va_list ap;
	int cnt;

	va_start(ap, fmt);
	cnt = vcprintf(fmt, ap);
	va_end(ap);

	return cnt;
}

打印函数需要调用 vcprintf 函数,接着看vcprintf 函数的实现


int
vcprintf(const char *fmt, va_list ap)
{
	struct printbuf b;

	b.idx = 0;
	b.cnt = 0;
	vprintfmt((void*)putch, &b, fmt, ap);
	sys_cputs(b.buf, b.idx);

	return b.cnt;
}

可以看到vcprintf需要调用两个函数:vprintfmat 和 sys_cputs 


以下是vprintfmat 函数的代码

void
vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)//caller : vprintfmt((void*)putch, &cnt, fmt, ap);
{
	register const char *p;
	register int ch, err;
	unsigned long long num;
	int base, lflag, width, precision, altflag;
	char padc;

	while (1) {
		while ((ch = *(unsigned char *) fmt++) != '%') {
			if (ch == '\0')
				return;
			putch(ch, putdat);
		       }

		// Process a %-escape sequence
		padc = ' ';
		width = -1;
		precision = -1;
		lflag = 0;
		altflag = 0;
	reswitch:
		switch (ch = *(unsigned char *) fmt++) {

		// flag to pad on the right
		case '-':
			padc = '-';
			goto reswitch;

		// flag to pad with 0's instead of spaces
		case '0':
			padc = '0';
			goto reswitch;

		// width field
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			for (precision = 0; ; ++fmt) {
				precision = precision * 10 + ch - '0';
				ch = *fmt;
				if (ch < '0' || ch > '9')
					break;
			}
			goto process_precision;

		case '*':
			precision = va_arg(ap, int);
			goto process_precision;

		case '.':
			if (width < 0)
				width = 0;
			goto reswitch;

		case '#':
			altflag = 1;
			goto reswitch;

		process_precision:
			if (width < 0)
				width = precision, precision = -1;
			goto reswitch;

		// long flag (doubled for long long)
		case 'l':
			lflag++;
			goto reswitch;

		// character
		case 'c':
			putch(va_arg(ap, int), putdat);
			break;

		// error message
		case 'e':
			err = va_arg(ap, int);
			if (err < 0)
				err = -err;
			if (err >= MAXERROR || (p = error_string[err]) == NULL)
				printfmt(putch, putdat, "error %d", err);
			else
				printfmt(putch, putdat, "%s", p);
			break;

		// string
		case 's':
			if ((p = va_arg(ap, char *)) == NULL)
				p = "(null)";
			if (width > 0 && padc != '-')
				for (width -= strnlen(p, precision); width > 0; width--)
					putch(padc, putdat);
			for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--)
				if (altflag && (ch < ' ' || ch > '~'))
					putch('?', putdat);
				else
					putch(ch, putdat);
			for (; width > 0; width--)
				putch(' ', putdat);
			break;

		// (signed) decimal
		case 'd':
			num = getint(&ap, lflag);
			if ((long long) num < 0) {
				putch('-', putdat);
				num = -(long long) num;
			}
			base = 10;
			goto number;

		// unsigned decimal
		case 'u':
			num = getuint(&ap, lflag);
			base = 10;
			goto number;

		// (unsigned) octal
		case 'o':
			num = getuint(&ap,lflag);
                        base = 8;
                        goto number;
			break;

		// pointer
		case 'p':
			putch('0', putdat);
			putch('x', putdat);
			num = (unsigned long long)
				(uintptr_t) va_arg(ap, void *);
			base = 16;
			goto number;

		// (unsigned) hexadecimal
		case 'x':
			num = getuint(&ap, lflag);
			base = 16;
		number:
			printnum(putch, putdat, num, base, width, padc);
			break;

		// escaped '%' character
		case '%':
			putch(ch, putdat);
			break;

		// unrecognized escape sequence - just print it literally
		default:
			putch('%', putdat);
			for (fmt--; fmt[-1] != '%'; fmt--)
				/* do nothing */;
			break;
		}
	}
}


从vprintfmat函数的代码可以看出,他主要调用的是putch,所以下面再让我们看一下putch的代码

static void
putch(int ch, struct printbuf *b)
{
	b->buf[b->idx++] = ch;
	if (b->idx == 256-1) {
		sys_cputs(b->buf, b->idx);
		b->idx = 0;
	}
	b->cnt++;
}


可以看到putch中主要掉用的是sys_cputs 函数,是不是似曾相识?回头看一下vcprintf函数,就会看到sys_cputs 函数!因此抛开细节,sys_cputs函数是关键!


好了让我们看看sys_cputs函数的真面目

void
sys_cputs(const char *s, size_t len)
{
	syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}


大吃一惊,我还以为他会是几百行呢。原来它也是调用了另一个函数 : syscall

不过看这个函数名称,好像离我们的系统调用很近了!

好,我们接着看下面的syscall函数源码

static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)// interface
{
	int32_t ret;

	asm volatile("int %1\n"
		: "=a" (ret)
		: "i" (T_SYSCALL),
		  "a" (num),
		  "d" (a1),
		  "c" (a2),
		  "b" (a3),
		  "D" (a4),
		  "S" (a5)
		: "cc", "memory");

	if(check && ret > 0)
		panic("syscall %d returned %d (> 0)", num, ret);

	return ret;
}

非常遗憾是看不懂的一段内联汇编,没事我们可以看看汇编后的汇编代码

 800a18:	55                   	push   %ebp
  800a19:	89 e5                	mov    %esp,%ebp
  800a1b:	57                   	push   %edi
  800a1c:	56                   	push   %esi
  800a1d:	53                   	push   %ebx


	asm volatile("int %1\n"
  800a1e:	b8 00 00 00 00       	mov    $0x0,%eax
  800a23:	8b 4d 0c             	mov    0xc(%ebp),%ecx
  800a26:	8b 55 08             	mov    0x8(%ebp),%edx
  800a29:	89 c3                	mov    %eax,%ebx
  800a2b:	89 c7                	mov    %eax,%edi
  800a2d:	89 c6                	mov    %eax,%esi
  800a2f:	cd 30                	int    $0x30

简单看一下这段代码,前面部分是建立栈帧。asm volatile部分的代码从 0x800ale地址开始。前面都是寄存器操作,看后面,有一个int 30,软中断。通过JOS源码可以知道,JOS的IDT对应的系统调用号是0x30。 这样我们就可以理解int 30 了!


其实我之前一直不太理解的问题是,int 30之后 系统怎么知道程序请求的具体是哪一个系统调用呢?我也知道是通过eax寄存器中的参数确定,但我不知道用户进程是怎么设置eax中的参数的,如今读了JOS源码才算知道。原来,对于用户进程调用的库函数,所有的库函数都有一个公共的接口(这里就是syscall函数),于是就可以通过这个接口,设置eax寄存器中的参数,使得各个库函数分别对应到各自的系统调用中。而这一切对用户程序来说都是透明的,他根本不知道什么是eax。


PS:以上代码源自MIT JOS系统源码



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值