天天看点

Linux内核用户权限的实现,Linux内核设计与实现(6)---系统调用

现代操作系统中,内核提供了用户进程和内核进程交互的一组接口,让app可以受限的访问硬件资源,提供进程间通信机制,实际上主要是为了保证系统稳定可靠,避免应用程序do whatever they want.

1.与内核通信

系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要作用:

①为用户空间提供了一种硬件的抽象接口;

②保证了系统的稳定和安全,可以给予权限,用户对访问进行裁决;

③每个进程都运行在虚拟系统中;

在Linux中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,是内核唯一的合法入口;实际上像设备文件和/proc之类的方式,也是通过系统调用进行访问的。

2  API、POSIX和C库

应用程序通过在用户空间实现的应用编程接口(API),而不是直接通过系统调用来编程。

因为API实际上不需要和系统调用对应,一个API可以实现成一个系统调用,也可以通过调用多个系统调用来实现,也可以完全不用。POSIX、API、C库及系统调用关系如下

Linux内核用户权限的实现,Linux内核设计与实现(6)---系统调用

程序员只跟API打交道,内核只跟系统调用打交道;即内核提供机制,API提供策略。

C库实现了大部分的POSIX标准API.

3.系统调用

系统调用一般用返回0来表示成功,返回负数表明错误,错误码写入errno全局变量,用peeror()库函数可以把错误码转变成错误字符串.

举一例,获取进程ID号的系统调用getpid()

点击(此处)折叠或打开

asmlinkage long sys_getpid(void)

{

return current->tgid;

}

①asmlinkage限定词是编译器指令,告知编译器仅从堆栈中提取函数的参数;

②内核返回long,用户空间返回int,是为了保证32位/64位系统兼容;

③get_pid在内核被定义为sys_getpid(),内核对系统调用都是如此定义的;

(1)系统调用号

Linux中,每个系统调用号被赋予一个唯一的系统调用号,进程不会提及系统调用名称,而是用系统调用号来关联具体的系统调用。

一个系统调用号一旦被分配,不能随意变更;用sys_ni_syscall()来补缺已经删除的调用号;

系统调用号保持在unsigned

long sys_call_table[NR_syscalls];

(2)系统调用的性能

Linux上下文切换时间很短,进出内核都被优化的简洁高效;系统调用处理程序和每个系统调用本身都非常简洁,所以Linux系统调用比许多其他操作系统都执行的快。

(3)系统调用处理程序

通过软中断引发一个异常,促使系统切换到内核态,执行异常处理程序代码;这个异常处理程序就是系统调用处理程序system_call()。

①找到指定的系统调用

X86上是通过eax把系统调用号传给内核,system_call()通过查找sys_call_table[]找到对应的系统调用

②参数传递

Ebx,ecx,edx,esi和edi依次存放前五个参数,若需要六个以上参数,用单独寄存器指向这些参数在用户空间地址的指针。通过eax存放返回值。

4.系统调用的实现

(1)决定用途,每个系统调用功能应该单一明确,不提倡多用途系统调用。系统调用参数,返回值和错误码都要明确,不要对机器字节长度和字节序做假设。

(2)参数验证:内核必须保证

①指向用户空间内存的指针,内核不能直接访问;

②指针指向的内存在用户进程空间里,内核不能读其他进程空间;

③内存不能绕过访问限制:可读内存标记为可读,可写标记为可写,可执行标记为可执行

内核用copy_to_user()和copy_from_user()来从用户空间读写数据,都是把第二个参数指定位置数据传送到第一个参数指定位置,长度由第三个参数决定。执行失败,返回未传送字节,成功返回0。copy_to_user()和copy_from_user()都可能引起休眠。

④检查权能,针对合法权限,比如if (!capable(CAP_SYS_BOOT))

return –EPERM;

(3)内核执行系统调用时处于进程上下文,current指针指向引发系统调用的那个进程。能够休眠,所以系统调用必须是可重入的。

(4)往系统添加一个系统调用的一个简单实例

①添加系统调用名字函数名字sys_mytest,一般在calls.S或者entry.S

.long       sys_get_mempolicy

.long       sys_set_mempolicy

.long   sys_mytest

②在unistd.h添加系统调用号,322

#define __NR_get_mempolicy             (__NR_SYSCALL_BASE+320)

#define __NR_set_mempolicy             (__NR_SYSCALL_BASE+321)

#define __NR_mytest

(__NR_SYSCALL_BASE+322)

③实现系统调用函数

点击(此处)折叠或打开

asmlinkage long sys_mytest(struct testsys __user *buf)

{

#if 1

struct testsys pbuf_kernel;

copy_from_user(&pbuf_kernel,buf,sizeof(pbuf_kernel));

pbuf_kernel.cmd += 1;

pbuf_kernel.value += 2;

copy_to_user(buf,&pbuf_kernel,sizeof(pbuf_kernel));

#endif

printk("---this is my test about sys_call!\r\n");

return 0;

}

④在syscalls.h做系统调用函数声明

点击(此处)折叠或打开

asmlinkage long sys_keyctl(int cmd, unsigned long arg2, unsigned long arg3,

unsigned long arg4, unsigned long arg5);

asmlinkage long sys_mytest(struct testsys __user *buf);

⑤app测试

点击(此处)折叠或打开

#include

#include

#include

#include

#define __NR_mytest 322

struct testsys{

int cmd;

int value;

};

int main(void)

{

struct testsys mysys;

mysys.cmd = 2;

mysys.value = 2;

//syscall(__NR_mytest);

syscall(322,&mysys);

printf("mysys.cmd:%d.\nmysys.value:%d\n",mysys.cmd,mysys.value);

return 0;

}

测试结果:

Linux内核用户权限的实现,Linux内核设计与实现(6)---系统调用

5.添加系统调用

优点有:

①系统调用创建容易,且使用方便;

②Linux系统调用高性能显而易见

缺点是:

①需要一个系统调用号,这个需要官方分配

②系统调用被加入稳定内核固化后,接口不能改变;

③需要将系统调用分别分配到各种体系结构去(与硬件相关)

④在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用

⑤在主内核树之外很难维护

⑥如果只进行简单信息交换,系统调用大材小用了。所以尽管建立一个系统调用非常容易,但是不建议这么做,替代方法:

①实现一个设备节点,并对此实现read()和write(),ioctl()来进行操作

②像信号量这样的某些接口,可以用文件描述符来表示

③把增加的信息作为一个文件放在sysfs的合适位置

Linux尽量使系统调用简洁,事实上Linux已经是一个相对稳定并且功能已经较为完善的操作系统。