(六)嵌入式:UCLinux下新增系统调用的实现

版权声明:wahahaguolinaiyou https://blog.csdn.net/wahahaguolinaiyou/article/details/84980531

开发环境见前面声明!!!!
1.知识扩展
1.1 Linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
系统调用,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件等。从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
1.2 系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:
1.它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。
2.系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。
3.每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的唯一手段;除异常和中断外,它们是内核惟一的合法入口。
1.3 通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
1.4 补充:
一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。
在Unix世界中,最流行的应用编程接口是基于POSIX标准的,其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX而定义的API函数和系统调用之间有着直接关系。
2.使用系统调用实现重启的功能
实现一个新的系统调用的第一步是决定他的用途。本实验要实现系统调用的重启功能,就要在内核代码中更新系统调用的系统调用表。 然后要决定系统调用的名字,这个名字就是你编写用户程序想使用的名字,比如我们取一个简单的名字:mysyscall。一旦这个名字确定下来了,那么在系统调用中的几个相关名字也就确定下来了。 系统调用的编号名字:__NR_mysyscall;内核中系统调用的实现程序的名字:sys_mysyscall;本实验取名为restart.系统调用的编号名字:__NR_restart;内核中系统调用的实现程序的名字:sys_restart;

2.1实现新的系统调用。无论何种配置,该系统调用都必须编译到核心的内核映象中去,所以我们把它放进kernel/sys.c文件中。先编写实现软件复位的程序,实现系统调用sys_restart()的函数功能。如图所示:
linux-2.4.x/kernel/sys.c
在这里插入图片描述
内核调用函数列表,由指向实现各种系统调用的内核函数的函数指针组成的表。记录在linux-2.4.x/arch/armnommu/kernel/calls.S 下面。在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作。从0开始算起,系统调用在该表中的位置就是它的系统调用号。我把新的系统调用加到这个表的末尾,如图所示:
linux-2.4.x/arch/armnommu/kernel/calls.S
在这里插入图片描述
虽然没有明确地指定编号,但加入的这个系统调用被按照次序分配给了222这个系统调用号。对于每种需要支持的体系结构,我们都必须将自己的系统调用加人到其系统调用表中去。每种体系结构不需要对应相同的系统调用号。
接下来,添加一个系统调用号,这里面每一个宏就是一个系统调用号,本实验添加的对应系统调用表里面的是222号。
linux-2.4.x/include/asm-armnommu/unistd.h
在这里插入图片描述
然后在头文件里面声明外部函数,export出来供内核其他部分共同使用。
linux-2.4.x/include/asm-armnommu/arch-firefox/system.h
在这里插入图片描述

uClibc/libc/sysdeps/linux/common/syscalls.c
在这里插入图片描述
2.2系统调用添加完成后,内核中有了函数restart()功能,要使用内核的函数,可以在内核,可以修改内核代码来调用,那么修改内核user/sash/reboot.c代码,如图所示:
在这里插入图片描述
使其调用restart,然后在内核配置时使能这个命令,这样在文件系统里面就出现了reboot的命令,如图所示,

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们把新的内核和文件系统下载到开发板之后,可以看到有了reboot命令,运行一下,系统自动重启,实现了软件复位的功能。
在这里插入图片描述

2.3 接下来要编写测试程序,在用户态通过系统调用,实现软件复位的功能。首先在user里面新建一个文件夹syscall,如图所示:

在这里插入图片描述
修改Makefile 文件,使其能够被编译,如图所示:

在这里插入图片描述
然后进入syscall,创建文件syscall.c 和 Makefile,并编写,如图所示:
在这里插入图片描述
在这里插入图片描述

然后make编译,编译完成后,查看romfs/bin目录里面有没有这个命令,如图所示:
在这里插入图片描述
这样再次下载到开发板,测试。如图示:
在这里插入图片描述
这样第一个实验结束,实现了内核添加新的系统调用,也实现了用户态对内核函数的调用。

  1. 按键控制实现软件复位
    由前面的LED实验,已经了解到GPIO的使用,我们查阅了相应的手册资料之后,可以知道板子上的GPIO0连接到的是按键S2,GPIO1连接到的是LED5。如图3-1所示:
    在这里插入图片描述

图2-1 GPIO的引脚关系

然后查阅手册知道了GPIO的使用流程,GPIO的基地址,寄存器偏移地址,如图2-2和2-3所示:
在这里插入图片描述
图2-2 GPIO的基地址
在这里插入图片描述
图2-3 GPIO的寄存器偏移量

那开始添加按键功能,开始读取GPIO的输入寄存器的值,如果按键按下之后,对应的输入寄存器的对应位会写1,也就是按键按下后会对GPIO口输入一个高电平,这个1在按键对应的位置也就是GPIO1。识别到按键按下超过4s后,就执行复位。修改代码如下所示:


#include<stdio.h>
#include<unistd.h>
#include<sys/syscall.h>
#include<sys/types.h>
int main()
{
	unsigned short inputval;
	int cnt = 1;
	/***设置GPIO模式为输出模式**/
	*((unsigned short *)(0x8000d000+0x00)) = 0x5555;
	*((unsigned short *)(0x8000d000+0x04)) = 0x5555;
	/***设置哪一个引脚输出--(每一位0是输出模式,1是输入模式)***/
	*((unsigned short *)(0x8000d000+0x08)) = 0xffff;
	/*检查输入寄存器的值,如果里面对的值与上0x02,即0bxxxx10,结果是0bxxxx10-即第二位是1,那	么说明按键按动*/
	while(1)
	{	
		inputval = *((unsigned short *)(0x8000d000+0x10));
		if((inputval&0x02)!=0x02)
		{
			printf("please press the key\n");
			break;
		}	
		else
			sleep(3);
		printf("the key has pressed 4 second \n");
		if((inputval&0x02)==0x02)
		{
			printf("this is my sysacall\n");
			restart(0xffff0000);
		}
		else 
		{
			printf("Oh,you are not careful,the key is 0\n");
			break;
		}		
	}
	return 0;
}

即首先设置首先要配置GPIOselect寄存器,然后使其的选择输出寄存器,那么配置每一位都是B01,选择寄存器有两个,每一个16位,那么两个选择寄存器的值都要设置为0x5555,跟LED实验保持一致,然后要设置GPIO的引脚输入输出模式,即设置寄存器GPIOOutputEN,没用到LED,所以全部设置为输入模式。然后读取GPIO的输入寄存器的值,判断是否按下按键持续4s。
如图所示,不按按键的时候,会提示没有按下,按住不松开超过4秒,就会执行软件复位功能。

总结:
此次实验流程,从用户态到内核态的调用,很好的加深了我们对内核的理解,也明白了什么是系统调用。用户态和内核态之间的切换和联系,都是我们以前从来没有过的认识。本次实验对我们能力的提升有很大的帮助,讲解了添加新的系统调用的方法,让我们自己完成添加系统调用的功能,以及添加按键的功能,这都既是对以前知识的复习,也是在加深新知识的理解。很好的提升了自己的开发能力,慢慢的形成自己的开发路线。
这次课程,我对于系统调用的理解是操作系统提供给用户程序调用的一组“特殊”接口。我们编写的用户程序可以通过接口来获得操作系统内核提供的服务,系统调用相当于一个内核与用户空间程序交互中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。用户空间和内核空间分开,让系统的数据和用户的数据互不干扰,保证系统的稳定性。分开存放,管理上很方便,而更重要的是,将用户的数据和系统的数据隔离开,就可以对两部分的数据的访问进行控制。这样就可以确保用户程序不能随便操作系统的数据,这样防止用户程序误操作或者是恶意破坏系统。

分析系统调用和库函数:
库函数也就是我们通常所说的应用编程接口API,它其实就是一个函数定义,比如常见read()、write()等函数说明了如何获得一个给定的服务,但是系统调用是通过软中断向内核发出一个明确的请求,再者系统调用是在内核完成的,而用户态的函数是在函数库完成的。系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。
库函数可以理解为是对系统调用的一层封装。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更方便于编码。库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。可以参考下图来理解库函数与系统调用的关系。
这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。

猜你喜欢

转载自blog.csdn.net/wahahaguolinaiyou/article/details/84980531
今日推荐