我如果能在内核中很方便地使用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。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值