linux内核空间与用户空间

良好的程序设计方法是:重视上层的业务逻辑操作,而尽可能避免底层复杂的实现细节

 

 

 

Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

 

内核态:在内核空间执行,通常是驱动程序,中断相关程序,内核调度程序,内存管理及其操作程序。

用户态:用户程序运行空间。

Linux操作系统中主要采用了0和3两个特权级,分别对应的就是内核态和用户态。运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

现代的计算机体系结构中存储管理通常都包含保护机制。提供保护的目的,是要避免系统中的一个任务访问属于另外的或属于操作系统的存储区域。如在IntelX86体系中,就提供了特权级这种保护机制,通过特权级别的区别来限制对存储区域的访问。 基于这种构架,Linux操作系统对自身进行了划分:一部分核心软件独立于普通应用程序,运行在较高的特权级别上,(Linux使用Intel体系的特权级3来运行内核。)它们驻留在被保护的内存空间上,拥有访问硬件设备的所有权限,Linux将此称为内核空间。

相对的,其它部分被作为应用程序在用户空间执行。它们只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,不能直接访问硬件,不能直接访问内核空间,当然还有其他一些具体的使用限制

用户程序和内核的信息交换是双向的,也就是说既可以主动从用户空间向内核空间发送信息,也可以从内核空间向用户空间提交数据。当然,用户程序也可以主动地从内核提取数据。下面我们就针对内核和用户交互数据的方法做一总结、归纳。

通常采用以下模式:首先,编写内核服务程序利用内核空间提供的权限和服务来接收、处理和缓存数据;然后编写用户程序来和先前完成的内核服务程序交互,具体来说,可以利用用户程序来配置内核服务程序的参数,提取内核服务程序提供的数据,当然,也可以向内核服务程序输入待处理数据

信息交互按信息传输发起方可以分为用户向内核传送/提取数据和内核向用户空间提交请求两大类,先来说说:

 

用户级程序主动发起的信息交互

 

A编写自己的系统调用

 

从前文可以看出,系统调用是用户级程序访问内核最基本的方法。目前linux大致提供了二百多个标准的系统调用(参见内核代码树中的include/ asm-i386/unistd.h和arch/i386/kernel/entry.S文件),并且允许我们添加自己的系统调用来实现和内核的信息交换。比如我们希望建立一个系统调用日志系统,将所有的系统调用动作记录下来,以便进行入侵检测。此时,我们可以编写一个内核服务程序。该程序负责收集所有的系统调用请求,并将这些调用信息记录到在内核中自建的缓冲里。我们无法在内核里实现复杂的入侵检测程序,因此必须将该缓冲里的记录提取到用户空间。最直截了当的方法是自己编写一个新系统调用实现这种提取缓冲数据的功能。当内核服务程序和新系统调用都实现后,我们就可以在用户空间里编写用户程序进行入侵检测任务了,入侵检测程序可以定时、轮训或在需要的时候调用新系统调用从内核提取数据,然后进行入侵检测了。

B编写驱动程序

Linux/UNIX的一个特点就是把所有的东西都看作是文件(every thing is a file)。系统定义了简洁完善的驱动程序界面,客户程序可以用统一的方法透过这个界面和内核驱动程序交互。而大部分系统的使用者和开发者已经非常熟悉这种界面以及相应的开发流程了。

驱动程序运行于内核空间,用户空间的应用程序通过文件系统中/dev/目录下的一个文件来和它交互。这就是我们熟悉的那个文件操作流程:open() —— read() —— write() ——ioctl() —— close()。(需要注意的是也不是所有的内核驱动程序都是这个界面,网络驱动程序和各种协议栈的使用就不大一致,比如说套接口编程虽然也有open()close()等概念,但它的内核实现以及外部使用方式都和普通驱动程序有很大差异。)

C: 使用proc 文件系统

proc是Linux提供的一种特殊的文件系统,推出它的目的就是提供一种便捷的用户和内核间的交互方式。它以文件系统作为使用界面,使应用程序可以以文件操作的方式安全、方便的获取系统当前运行的状态和其它一些内核数据信息。

proc文件系统多用于监视、管理和调试系统,我们使用的很多管理工具如ps,top等,都是利用proc来读取内核信息的。除了读取内核信息,proc文件系统还提供了写入功能。所以我们也就可以利用它来向内核输入信息。比如,通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数;再如,通过下面这条指令:

echo 1 > /proc/sys/net/ip_v4/ip_forward

开启内核中控制IP转发的开关,我们就可以让运行中的Linux系统启用路由功能。类似的,还有许多内核选项可以直接通过proc文件系统进行查询和调整。

除了系统已经提供的文件条目,proc还为我们留有接口,允许我们在内核中创建新的条目从而与用户程序共享信息数据。比如,我们可以为系统调用日志程序(不管是作为驱动程序也好,还是作为单纯的内核模块也好)在proc文件系统中创建新的文件条目,在此条目中显示系统调用的使用次数,每个单独系统调用的使用频率等等。我们也可以增加另外的条目,用于设置日志记录规则,比如说不记录open系统调用的使用情况等

D: 使用虚拟文件系统

有些内核开发者认为利用ioctl()系统调用往往会似的系统调用意义不明确,而且难控制。而将信息放入到proc文件系统中会使信息组织混乱,因此也不赞成过多使用。他们建议实现一种孤立的虚拟文件系统来代替ioctl()和/proc,因为文件系统接口清楚,而且便于用户空间访问,同时利用虚拟文件系统使得利用脚本执行系统管理任务更家方便、有效。

我们举例来说如何通过虚拟文件系统修改内核信息。我们可以实现一个名为sagafs的虚拟文件系统,其中文件log对应内核存储的系统调用日志。我们可以通过文件访问特普遍方法获得日志信息:如

# cat /sagafs/log

使用虚拟文件系统——VFS实现信息交互使得系统管理更加方便、清晰。但有些编程者也许会说VFS 的API 接口复杂不容易掌握,不要担心2.5内核开始就提供了一种叫做libfs的例程序帮助不熟悉文件系统的用户封装了实现VFS的通用操作。有关利用VFS实现交互的方法看参考资料。

E: 使用内存映像

Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。而在使用系统调用交互信息时,在整个操作过程中必须有一步数据拷贝的工作——或者是把内核数据拷贝到用户缓冲区,或只是把用户数据拷贝到内核缓冲区——这对于许多数据传输量大、时间要求高的应用,这无疑是致命的一击:许多应用根本就无法忍受数据拷贝所耗费的时间和资源。

我们曾经为一块高速采样设备开发过驱动程序,该设备要求在20兆采样率下以1KHz的重复频率进行16位实时采样,每毫秒需要采样、DMA和处理的数据量惊人,如果要使用数据拷贝的方法,根本无法达成要求。此时,内存映像成为唯一的选择:我们在内存中保留了一块空间,将其配置成环形队列供采样设备DMA输出数据。再把这块内存空间映射到在用户空间运行的数据处理程序上,于是,采样设备刚刚得到并传送到主机上的数据,马上就可以被用户空间的程序处理。

实际上,内存影射方式通常也正是应用在那些内核和用户空间需要快速大量交互数据的情况下,特别是那些对实时性要求较强的应用。X window系统的服务器的虚拟内存区域,就可以被看做是内存映像用法的一个典型例子:X服务器需要对视频内存进行大量的数据交换,相对于lseek/write来说,将图形显示内存直接影射到用户空间可以显著提高效能。

并不是任何类型的应用都适合mmap,比如像串口和鼠标这些基于流数据的字符设备,mmap就没有太大的用武之地。并且,这种共享内存的方式存在不好同步的问题。由于没有专门的同步机制可以让用户程序和内核程序共享,所以在读取和写入数据时要有非常谨慎的设计以保证不会产生干绕。

mmap完全是基于共享内存的观念了,也正因为此,它能提供额外的便利,但也特别难以控制

由内核主动发起的信息交互

在内核发起的交互中,我们最关心和感兴趣的应该是内核如何向用户程序发消息,用户程序又是怎样接收这些消息的,具体问题通常集中在下面这几个方面:内核可否调用用户程序?是否可以通过向用户进程发信号来告知用户进程事件发生?

前面介绍的交互方法最大的不同在于这些方式是由内核采取主动,而不是等系统调用来被动的返回信息的。

A 从内核空间调用用户程序。

即使在内核中,我们有时也需要执行一些在用户级才提供的操作:如打开某个文件以读取特定数据,执行某个用户程序从而完成某个功能。因为许多数据和功能在用户空间是现有的或者已经被实现了,那么没有必要耗费大量的资源去重复。此外,内核在设计时,为了拥有更好的弹性或者性能以支持未知但有可能发生的变化,本身就要求使用用户空间的资源来配合完成任务。比如内核中动态加载模块的部分需要调用kmod。但在编译kmod的时候不可能把所有的内核模块都订下来(要是这样的话动态加载模块就没有存在意义了),所以它不可能知道在它以后才出现的那些模块的位置和加载方法。因此,模块的动态加载就采用了如下策略:加载任务实际上由位于用户空间的modprobe程序帮助完成——最简单的情形是modprobe用内核传过来的模块名字作为参数调用insmod。用这种方法来加载所需要的模块。

内核中启动用户程序还是要通过execve这个系统调用原形,只是此时的调用发生在内核空间,而一般的系统调用则在用户空间进行。如果系统调用带参数,那将会碰到一个问题:因为在系统调用的具体实现代码中要检查参数合法性,该检查要求所有的参数必须位于用户空间——地址处于0x0000000——0xC0000000之间,所以如果我们从内核传递参数(地址大于0xC0000000),那么检查就会拒绝我们的调用请求。为了解决这个问题,我们可以利用set_fs宏来修改检查策略,使得允许参数地址为内核地址。这样内核就可以直接使用该系统调用了。

例如:在kmod通过调用execve来执行modprobe的代码前需要有set_fs(KERNEL_DS):

......
set_fs(KERNEL_DS);

/* Go, go, go... */
if (execve(program_path, argv, envp) < 0)
return -errno;
上述代码中program_path 为"/sbin/modprobe",argv为{ modprobe_path, "-s", "-k", "--", (char*)module_name, NULL },envp为{ "HOME=/", "TERM=linux", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }。

从内核中打开文件同样使用带参数的open系统调用,所需的仍是要先调用set_fs宏。

B 利用brk系统调用来导出内核数据

内核和用户空间传递数据主要是用get_user(ptr)和put_user(datum,ptr)例程。所以在大部分需要传递数据的系统调用中都可以找到它们的身影。可是,如果我们不是通过用户程序发起的系统调用——也就是说,没有明确的提供用户空间内的缓冲区位置——的情况下,如何向用户空间传递内核数据呢?

显然,我们不能再直接使用put_user()了,因为我们没有办法给它指定目的缓冲区。所以,我们要借用brk系统调用和当前进程空间:brk用于给进程设置堆空间的大小。每个进程拥有一个独立的堆空间,malloc等动态内存分配函数其实就是进程的堆空间中获取内存的。我们将利用brk在当前进程(current process)的堆空间上扩展一块新的临时缓冲区,再用put_user将内核数据导出到这个确定的用户空间去。

还记得刚才我们在内核中调用用户程序的过程吗?在那里,我们有一个跳过参数检查的操作,现在有了这种方法,可以另辟蹊径了:我们在当前进程的堆上扩展一块空间,把系统调用要用到的参数通过put_user()拷贝到新扩展得到的用户空间里,然后在调用execve的时候以这个新开辟空间地址作为参数,于是,参数检查的障碍不复存在了。

char * program_path = "/bin/ls" ;

/* 找到当前堆顶的位置*/ 
mmm=current->mm->brk;
/* 用brk在堆顶上原扩展出一块256字节的新缓冲区*/
ret = brk(*(void)(mmm+256));
/* 把execve需要用到的参数拷贝到新缓冲区上去*/
put_user((void*)2,program_path,strlen(program_path)+1);
/* 成功执行/bin/ls程序!*/ 
execve((char*)(mmm+2));
/* 恢复现场*/
tmp = brk((void*)mmm);

这种方法没有一般性(具体的说,这种方法有负面效应吗),只能作为一种技巧,但我们不难发现:如果你熟悉内核结构,就可以做到很多意想不到的事情!

C: 使用信号:

信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这时内核通过发送SIGKILL信号通知进程终止,内核发送信号使用send_sign(pid,sig)例程,可以看到信号发送必须要事先知道进程序号(pid),所以要想从内核中通过发信号的方式异步通知用户进程执行某项任务,那么必须事先知道用户进程的进程号才可。而内核运行时搜索到特定进程的进程号是个费事的工作,可能要遍历整个进程控制块链表。所以用信号通知特定用户进程的方法很糟糕,一般在内核不会使用。内核中使用信号的情形只出现在通知当前进程(可以从current变量中方便获得pid)做某些通用操作,如终止操作等。因此对内核开发者该方法用处不大。

类似情况还有消息操作。这里不罗嗦了。

总结  由用户级程序主动发起的信息交互,无论是采用标准的调用方式还是透过驱动程序界面,一般都要用到系统调用。而由内核主动发起信息交互的情况不多。也没有标准的界面,操作大不方便。所以一般情况下,尽可能用本文描述的前几种方法进行信息交互

 主要的IPC机制:管道(Pipe)和命名管道(Name Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),信号量(Semaphore),套接字(Socket)。通过这些IPC机制,用户空间进程之间可以完成互相通信。而为了完成内核空间和用户空间之间的通信,linux提供了基于socket的Netlink通信机制,可以实现内核与用户空间数据的及时交换。

  • 管道(PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(父子进程)的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
  • 有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号(Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存(Shared Memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  • 套接字(Socket): 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。

一、Copy_to_user( to, &from, sizeof(from))

To:用户空间函数  (可以是数组)

From:内核空间函数(可以是数组)

sizeof(from):内核空间要传递的数组的长度

 

二、Copy_from_user(&from , to , sizeof(to) )

To:用户空间函数  (可以是数组)

From:内核空间函数(可以是数组)

sizeof(from):内核空间要传递的数组的长度

 

成功返回0,

失败返回失败数目。

以上内容从网文摘录。。。。。

 

原文地址:https://www.cnblogs.com/chjgongzuo/p/7159619.html