Linux的系统调用:原理+三种执行方式

参考书:《Linux内核模块开发技术指南》

1.系统调用的执行过程

在Linux中,由用户空间发起系统调用,以访问内核空间的外设、数据或其他资源,这种访问是通过系统调用间接访问的。在X86的64位处理器上,系统调用对应的汇编语句是syscall。应用程序执行syscall汇编语句后,Linux系统会进入内核空间执行相应的处理函数,处理完成后,会将系统调用的处理结果返回给应用程序,如下图所示。
在这里插入图片描述
每一个系统调用都有一个系统调用号,例如在X86的64位系统中,read对应的系统调用号是0,write是1,open是2,close是3。
在Linux中存在一个系统调用表,这个系统调用表在内核源码的变量名是sys_call_table。sys_call_table作为一个指针数组,它的每一个成员是一个函数指针,数组元素的编号对应系统调用号。例如sys_call_table[0]对应的就是read系统调用最终在内核中执行的函数(read的系统调用号是0)。同理,sys_call_table[1]对应的是write系统调用的执行函数(write的系统调用号是1)。
综合以上描述可以看出,应用程序在执行系统调用时,需要传入的参数应该有:系统调用号以及对应系统调用的参数。系统调用号用于从系统调用表中取出相应的执行函数,而系统调用的参数用于系统调用执行函数的处理过程,如下图所示。
在这里插入图片描述
上图在用户空间执行系统调用,假设系统调用号是1,1对应write系统调用,而write系统调用的函数原型是:ssize_t write(int fd, const void *buf, size_t count)。第一个参数fd是文件描述符,第二个参数buf是将要写入的数据缓存,第三个参数count是数据长度。假设上图传入的文件描述符是1,数据缓存是“hello”字符串,数据长度是5。将这些参数传入并进入内核空间后,Linux内核会查找系统调用表编号是1的执行函数。而上图标号是1的执行函数是sys_write,这个函数就是write系统调用最终在内核中执行的函数。

2.系统调用的三种执行方式

在应用程序中,可以通过多种方式来执行系统调用,下面将以write系统调用为例,通过三种方式来执行该系统调用。
一、方式一:调用libc提供的上层接口
libc库提供了write函数来执行write系统调用,使用write函数的方式如下所示。

#include <unistd.h>
int main()
{
char *buf = "hello"; //变量buf保存了将要写入的字符串
//向标准输出写数据,写入的内容是变量buf保存的内容,数据长度是5字节
write(1, buf, 5);    
return 0;
}

上述源码直接通过libc库提供的write函数向文件描述符1写入一个字符串“hello”,文件描述符1是标准输出,编译、运行源码后,会看到命令行终端中会打印出字符串“hello”,如下图所示。
在这里插入图片描述
二、方式二:调用libc提供的syscall函数
libc库提供了一个专门用于系统调用的函数:syscall,函数原型如下:

int syscall(int number, ...)

该接口是一个可变参数的函数。第一个参数number是系统调用号,如果需要执行read系统调用,则填0,write系统调用填1……剩余的参数是系统调用对应的参数,如果是write系统调用,则剩余的参数分别为文件描述符、数据缓存指针、数据长度。函数返回值是系统调用执行后的返回值。
下面将通过syscall函数来执行write系统调用,实现与方式一(调用libc提供的上层接口)同样的功能,即向标准输出写入字符串“hello”。该程序如下所示。

#define _GNU_SOURCE  //使用syscall函数需定义该宏
#include <unistd.h>//使用syscall函数需引入头文件unistd.h和sys/syscall.h
#include <sys/syscall.h>
int main()
{
	char *buf = "hello";
	syscall(1, 1, buf, 5); //通过系统调用号是1的系统调用,向标准输出写入字符串
	return 0;
}

源码通过syscall系统调用向标准输出写入字符串“hello”。传入syscall的第一个参数是系统调用号1,1代表write系统调用。第二个参数是文件描述符,1代表标准输出。第三个参数是数据缓存。第四个参数是数据的长度,一共5字节。编译、执行该源码后,执行结果与方式一的执行结果一致。
三、方式三:调用汇编指令syscall
除了上述两种方法,还可通过汇编指令syscall来执行write系统调用。libc库里的syscall函数基于syscall汇编语句实现,syscall这个汇编语句在X86的64位系统中专门用于系统调用。示例程序将由两个源文件组成:test_asm_write.S和test_asm_write.c。其中,test_asm_write.S将使用汇编语句实现一个函数,这个函数将调用syscall语句执行系统调用。test_asm_write.c将调用test_asm_write.S实现的函数向标准输出中写入“hello”字符串,test_asm_write.c的实现如下所示。

//test_asm_write.c
//该函数将在test_asm_write.S实现,用于写数据
extern void my_write(int fd, char *buf, int len); 
int main()
{
	char *buf = "hello";
	//通过test_asm_write.S的my_write函数向标准输出写入“hello”字符串
	my_write(1, buf, 5);  
	return 0;
}

源码test_asm_write.c和方式一的源码几乎一样,只是将test_gcc_write.c中的write函数替换为my_write函数,这个函数将在test_asm_write.S文件中实现,会调用write系统调用执行实际的写操作。my_write函数有三个参数,第一个参数是文件描述符,第二个参数是数据缓存,第三个参数是数据的长度。
源文件test_asm_write.S实现了函数my_write,如下所示。

#将my_write函数声明为全局,这样在test_asm_write.c中可以通过extern导入
.global my_write 
my_write:         #my_write函数的实现
mov $1, %rax  #将立即数1放入rax寄存器
syscall       #执行syscall汇编语句
ret           #汇编函数返回需要用ret,和C语言的return功能类似

源文件test_asm_write.S的第1行通过.global语句将my_write函数声明为全局函数,声明后,在test_asm_write.c源文件才能将该函数通过extern导入。在第2行的“my_write:”标号下面就是my_write函数的实现,首先通过mov语句将立即数1放入rax寄存器中,rax寄存器中存放的值就是系统调用号,然后调用syscall汇编语句执行系统调用。执行syscall后,内核会根据rax寄存器的值执行对应的系统调用。此时rax寄存器中的值是1,对应write系统调用。
使用gcc编译这两个源文件,编译命令是:gcc -o test_asm_write test_asm_write.c test_asm_write.S。编译完成后,生成test_asm_write可执行文件。执行test_asm_write文件,结果与方式一的执行结果一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值