我如果能在内核中很方便地使用HIGHUSER内存该有多好...

需求

近日在工作中遇到一个需求,即:内核中需要保存一些用户信息(包括用户名,密码,登录时间等等),这些用户信息和TCP/IP协议栈的一个数据流进行绑定,用于决定针对数据包采取的动作。

必要性


着应用越来越多越来越复杂,在路由器或者网关中仅仅依靠数据包协议头的字段已经无法对一个数据包采取一个粒度更细的抉择,更多的时候,我们需要一些应用层
的信息。当然,采用内核态的深度包解析技术是可以解决的,将数据包通过PF_RING之类的技术捕获到用户态也是一种常规操作,但是这些技术均需要修改既
有的实现,比如将协议栈部署在用户态,修改应用等。诚然,用户态实现协议栈在朴素的UNIX看来是一种更加合理的做法,但是由于目前这么玩的还不算多,因
此只能继续完善内核协议栈。
       纯技术上的说法,你在内核态就不该处理业务!因此也不需要访问记有业务信息的内存,况且还是那么大的内存...唉,问题讨来绕去就是个圈子,如果我不需要那么大的内存,我干嘛要用用户内存啊?!
       现在的问题是,如何让数据包在内核协议栈的处理路径上访问应用层信息。

分析

关于底层的操作,比如如何将一个数据包和一个流关联,我不想多说,完全可以仅仅针对NEW状态的数据包查询应用信息表,然后将信息保存在conntrack结构中,后续的数据包restore出来即可,就像CONNMARK一样。

想法是简单的,但是问题总是一步步走来。起初,我希望在内核中分配一块内存,然后将用户信息注入进来,针对NEW状态的数据包去查询这个表。后来我觉得扩
展系统路由表或许更好,路由信息中除了下一跳信息之外,再加上一些应用信息,比如HTTP头,SSL状态之类的,再后来,由于我不想污染系统路由表,于是
干脆dup一份"路由表"。...这些都不涉及具体实现,即便是在我具体实现它们的时候,也没有碰到任何问题,直到我在内核中塞了10000张X.509
证书...
      
IA32架构中,我们知道内核一一映射的内存默认是896M,其上有一段空洞,然后是一段vmalloc空间,永久/临时映射空间,注意,这些空间都是极
其短缺的,记住,我们想往内存中写数据,就必须将它们映射到地址空间的某个段,对于内核线程而言,我们只能使用最多1G的虚拟地址空间(没有写错,是虚拟
地址空间!),这还不是最严重的,你要知道,1G是最多允许你使用的空间,除去内核数据结构已经使用的以及将来要使用的,给你留下的连续地址空间更加少,
不要指望使用vmalloc,它大小太有限了!以上这些还意味着一件事,就是你不能用kmalloc分配函数族中分配高端内存,因为它们都必须是和物理内
存一一映射的。
       内核内存根本就不是让你这么用的。
      
但是我们知道,高端内存是一片沃土,目前的服务器动辄好几十G的内存太常见了,可惜却不能在内核空间用...哦,不,可以用,办法是调用
alloc_pages族在HIGHUSER区域分配页面(注意,返回的不是地址,而是页面),然后需要对其操作的时候,将其map到临时映射区,读写数
据,然后解除映射,map下一个页面,读写数据,解除映射,map再下一个...直到你的读写操作完成....够了!此时,也许你能想到,64位架构中,
将解除1G内核地址空间的限制,但是治标不治本,随着你胃口的增大,面对比内核空间多得多的总地址空间大小,你很快就会吃光内核内存空间!注意,我们面临
的不是内存不够用,而是地址空间不够用,毕竟你要往数据结构里读写数据,必须将其映射到某个地址空间的地址才行,分步骤多次映射/读写/解除映射效率太
低,一次映射空间又不够,怎么办?

办法是有的!那就是在用户态通过应用程序分配一块大内存空间(此时可能还没有提交到物理内存,只是保留了一块映射区域),然后在内核中使用用户的这块地址空间进行读写操作。

问题

在内核中访问用户态的内存?这种用法比较少见,我知道Linux的AIO是这么玩的。反过来的操作却很常见,比如在内核中的一一映射空间kmalloc出一块内核内存,然后将其映射到用户态。

比较少见是一定的,因为在Linux中,所有的进程的地址空间的内核态部分都是共享的,用户态部分是隔离的,如果你想在内核中访问用户态内存,你必须指定
是要访问哪个用户进程的内存。即你必须指定一个mm_struct结构体,事实上,内核中是提供这种接口的,来自include/linux
/mmu_context.h:

#ifndef _LINUX_MMU_CONTEXT_H
#define _LINUX_MMU_CONTEXT_H

struct mm_struct;

void use_mm(struct mm_struct *mm);
void unuse_mm(struct mm_struct *mm);

#endif

做法

具体怎么实施呢?很方便。比如我在用户态部署了一个sqlite内存数据库,或者直接部署了一个
memcached,或者一个redis,不管是什么都行,我只需要将运行数据存储的守护进程的PID告知内核,然后在内核中取到该进程的
mm_struct,调用use_mm即可,此时就像在该存储进程中一样,因为地址空间已经switch到它了。用完了之后再unuse_mm(但是注
意,请看小贴士)。

小贴士

你不能在中断上下文调用use_mm去访问用户内存,因为会导致缺页,而缺页会睡眠,你要知
道,此时的current是一个任意上下文。以上只是问题的一方面,另一方面来自Linux的内核线程约定,Linux中认为内核线程是没有
mm_struct的,即其mm字段为NULL,如果你在任意上下文调用use_mm,完事后调用unuse_mm的话,即便没有发生缺
页,current的mm也会设置为NULL,这怎么可以?!current是谁你都不知道...因此,调用use_mm/unuse_mm时,你必须:
1.确保此时是可以睡眠的;
2.如果current的mm不为NULL,保存调用use_mm之前的mm,在unuse_mm之后再次use_mm(old_mm)。
关于Linux针对内核线程的约定,可以多说几句。

如果你调用kernel_thread创建一个内核线程,你会发现它的mm不一定是NULL,因为在fork操作中,新的task_struct完全是继
承其parent的,如果是在module的init中创建,其parent无疑就是insmod进程,它是一个用户态进程,因此这种方式创建的内核线程
的mm并非NULL!如果希望完全遵循Linux关于内核线程mm为NULL的约定,就必须使其parent为一个没有mm的内核线程,该线程哪里找呢?
难不成需要创建一个内核线程,然后手工将其mm设置为NULL(可以调用daemonize函数来完成,但是难道这样不麻烦吗)?不是这样的。Linux
在初启的时候就为创建“mm为NULL的”内核线程提供了基础设施。
      
Linux内核委托一个初启时进入用户态前生成的一个内核线程(当然没有mm_struct),来生成新的内核线程,该内核线程创建自0号线程。这个内核
线程就是kthreadd_task,以后再需要创建新的内核线程的时候,只需要将要创建的内核线程的一切参数(包括执行的函数,参数等)打个包排到一个
list中,然后唤醒kthreadd_task,之后kthreadd_task再从list中取出参数,调用kernel_thread创建内核线程
(此时新的内核线程的parent的mm即kthreadd_task的mm,为NULL)。所以说kernel_thread只是一个创建内核线程的底
层接口,而不是用户接口,用户接口是:

#define kthread_run(threadfn, data, namefmt, ...)               ({                                           struct task_struct *__k                                   = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__);     if (!IS_ERR(__k))                                   wake_up_process(__k);                           __k;                                   })

总之,使用kthread_run创建内核线程是正确的做法,内核将替你将创建的线程真正内核化并维护其生命周期,包括exit,信号处理等。
       另外,还有一个细节,即kthreadd_task必须是在init进程被fork/exec之后,否则它会抢掉进程号1,而这是不允许的,1号进程是init,这是UNIX的约定。所以,kthreadd_task进程号就成了2。

时间: 2024-08-14 16:29:55

我如果能在内核中很方便地使用HIGHUSER内存该有多好...的相关文章

我如果能在内核中很方便地使用HIGHUSER内存该有多好...一个例子

话说用户态访问内核内存很简单,将这块内核内存映射到用户地址空间即可.依托一个字符设备,实现其mmap回调函数,在用户进程打开那个设备,mmap之,很容易实现用户进程直接访问内核内存. 但是反过来呢?内核访问用户内存.理论上也很简单,因为所有进程的内核态地址空间都是共享的,所以想访问哪个进程的内存,就切换到那个进程的地址空间,这 将丝毫不会影响当前的执行流.事实上也是这么简单,use_mm就是干这个的,不过既然要切换地址空间,那么当前task的地址空间就必须是明确的,因此 就不能在任意上下文调用u

Linux内核中常见内存分配函数

1.原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分别为: l   页全局目录(Page Global Directory) l   页上级目录(Page Upper Directory) l   页中间目录(Page Middle Directory) l   页表(Page Table) 页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录

Linux内核中常见内存分配函数zz

https://blog.csdn.net/wzhwho/article/details/4996510 1.      原理说明 Linux内核中采用了一种同时适用于32位和64位系统的内存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系统中,用到了四级页表,如图2-1所示.四级页表分别为: l         页全局目录(Page Global Directory) l         页上级目录(Page Upper Directory) l         页中间目录(

Linux内核中的哈希表

Author:tiger-john Time:2012-12-20mail:[email protected]Blog:http://blog.csdn.net/tigerjb/article/details/8450995 转载请注明出处. 前言: 1.基本概念: 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表.

Linux内核中的jiffies及其作用介绍及jiffies等相关函数详解

在LINUX的时钟中断中涉及至二个全局变量一个是xtime,它是timeval数据结构变量,另一个则是jiffies,首先看timeval结构struct timeval{time_t tv_sec; /***second***/susecond_t tv_usec;/***microsecond***/}到底microsecond是毫秒还是微秒?? 1秒=1000毫秒(3个零),1秒=1000 000微秒(6个零),1秒=1000 000 000纳秒(9个零),1秒=1000 000 000

C语言在linux内核中do while(0)妙用之法

为什么说do while(0) 妙?因为它的确就是妙,而且在linux内核中实现是相当的妙,我们来看看内核中的相关代码: #define db_error(fmt, ...) do { fprintf(stderr, "(error): "); fprintf(stderr, fmt, ##__VA_ARGS__); } while (0) 这只是个普通的调试信息的输出,有人便会认为,你这不是多此一举吗?去掉do while(0)不一样也实现了吗?其实不然,我们看看例子就清楚了,尽管很

向linux内核中添加外部中断驱动模块

本文主要介绍外部中断驱动模块的编写,包括:1.linux模块的框架及混杂设备的注册.卸载.操作函数集.2.中断的申请及释放.3.等待队列的使用.4.工作队列的使用.5.定时器的使用.6.向linux内核中添加外部中断驱动模块.7.完整驱动程序代码.linux的内核版本为linux2.6.32.2. 一.linux模块的框架以及混杂设备相关知识 1.内核模块的框架如下图所示,其中module_init()(图中有误,不是modules_init)只有在使用insmod命令手动加载模块时才会被调用,

Linux内核中的软中断、tasklet和工作队列详解

[TOC] 本文基于Linux2.6.32内核版本. 引言 软中断.tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的"下半部"(bottom half)演变而来.下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者. 介绍这三种下半部实现之前,有必要说一下上半部与下半部的区别. 上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务.举个例子:在网络传输中,网卡接收到数据包这

Linux内核中namespace之PID namespace

前面看了LInux PCI设备初始化,看得有点晕,就转手整理下之前写的笔记,同时休息一下!!~(@^_^@)~ 这片文章是之前写的,其中参考了某些大牛们的博客!! PID框架的设计 一个框架的设计会考虑很多因素,相信分析过Linux内核的读者来说会发现,内核的大量数据结构被哈希表和链表链接起来,最最主要的目的就是在于查找.可想而知一个好的框架,应该要考虑到检索速度,还有考虑功能的划分.那么在PID框架中,需要考虑以下几个因素. 如何通过task_struct快速找到对应的pid 如何通过pid快