我如果能在内核中很方便地使用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。




 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1601782
相关文章
|
2月前
|
存储 安全 数据安全/隐私保护
3.2 Windows驱动开发:内核CR3切换读写内存
CR3是一种控制寄存器,它是CPU中的一个专用寄存器,用于存储当前进程的页目录表的物理地址。在x86体系结构中,虚拟地址的翻译过程需要借助页表来完成。页表是由页目录表和页表组成的,页目录表存储了页表的物理地址,而页表存储了实际的物理页框地址。因此,页目录表的物理地址是虚拟地址翻译的关键之一。在操作系统中,每个进程都有自己的地址空间,地址空间中包含了进程的代码、数据和堆栈等信息。为了实现进程间的隔离和保护,操作系统会为每个进程分配独立的地址空间。在这个过程中,操作系统会将每个进程的页目录表的物理地址存储在它自己的CR3寄存器中。当进程切换时,操作系统会修改CR3寄存器的值,从而让CPU使用新的页
3.2 Windows驱动开发:内核CR3切换读写内存
|
3月前
|
安全 Windows
3.3 Windows驱动开发:内核MDL读写进程内存
MDL内存读写是一种通过创建MDL结构体来实现跨进程内存读写的方式。在Windows操作系统中,每个进程都有自己独立的虚拟地址空间,不同进程之间的内存空间是隔离的。因此,要在一个进程中读取或写入另一个进程的内存数据,需要先将目标进程的物理内存映射到当前进程的虚拟地址空间中,然后才能进行内存读写操作。
3.3 Windows驱动开发:内核MDL读写进程内存
|
3月前
|
缓存 监控 Anolis
|
3月前
|
Web App开发 缓存 Linux
深入理解Linux内核内存管理机制与实现(下)
深入理解Linux内核内存管理机制与实现
|
3月前
|
缓存 Linux 程序员
深入理解Linux内核内存管理机制与实现(上)
深入理解Linux内核内存管理机制与实现
|
3月前
|
存储 算法 Linux
探索Linux内核内存伙伴算法:优化系统性能的关键技术!
探索Linux内核内存伙伴算法:优化系统性能的关键技术!
|
3月前
|
存储 编译器 Linux
解密Linux内核神器:内存屏障的秘密功效与应用方法(下)
解密Linux内核神器:内存屏障的秘密功效与应用方法(下)
|
27天前
|
Linux
|
1月前
|
监控 关系型数据库 MySQL
innodb_buffer_pool_instances 如何根据cpu和内存进行配置
`innodb_buffer_pool_instances` 是用于配置 InnoDB 缓冲池实例数的参数。每个实例都管理缓冲池的一部分,这有助于提高并发性能。通常,你可以根据系统的 CPU 和内存来调整这个参数,以获得更好的性能。 以下是一些建议和步骤,帮助你根据 CPU 和内存进行 `innodb_buffer_pool_instances` 的配置: 1. **了解系统资源:** 首先,了解系统的硬件资源,特别是内存和CPU。检查系统上可用的物理内存和 CPU 核心数量。 2. **考虑每个实例的大小:** 在配置 `innodb_buffer_pool_instances` 时,
|
1天前
|
弹性计算 大数据 测试技术
2024阿里云服务器租用价格表(CPU/内存/带宽/磁盘收费标准)
阿里云服务器分为轻量应用服务器和云服务器ECS,轻量适合个人开发者使用,搭建轻量级的网站、测试环境使用;专业级如大数据、科学计算、高并发网站等需要使用云服务器ECS。2024年阿里云服务器租用价格表出炉!云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、ECS u1实例2核4G、5M固定带宽、80G ESSD Entry盘优惠价格199元一年,轻量应用服务器2核2G3M带宽轻量服务器一年61元、2核4G4M带宽轻量服务器一年165元12个月、2核4G服务器30元3个月,幻兽帕鲁4核16G和8核32G服务器配置,云服务器ECS可以选择经济型e实例、通用算力u1实例、ECS计算型c7、通

相关产品

  • 云迁移中心