Linux驱动开发始末

SMx内核驱动开发笔记

- 背景

  • 硬件背景

为SMx开发Linux驱动。
SMx为与ARMv8核心以AXI和AHB挂接的SoC内部模块,用于SM3/4加速。
SMx功能包括SM3,SM4-ECB/CBC/CTR/CFB/OFB/GCM计算。
含有内部自用DMA,以及与SPAcc相似的内部连线接口。

  • 软件背景

目标系统Peta Linux, kernel 4.6, gcc-linux-linaro-5.2-arm64
主机系统Ubunbu 16.04, kernel 4.8
结构采用SPAcc相似结构,驱动内将Scatterlist数据结构映射为DMA可用的DDT。

- 结构框图

*--------------------*
* Testbench/用户程序 * <-- SMx test, smx_enc_cbc()...  
*--------------------* 
     |  
    \|/  
*----------*  
*  Socket  * <-- cmesg, msg, socket, 用户与内核的分界
*----------*  
     |  
    \|/  
+------------+    
| Crypto API | <-- crypto_tfm, ablkcipher_request, crypto_alg, smx_alg  
+------------+  
     |  
    \|/  
+------------------+  
| 初始化           | <-- smx_init_all()  
| 异步任务封包     | <-- job  
| 寄存器级阶段封装 | <-- smx_open(), set_context(), smx_operation()  
| SG-DDT映射       | <-- enqueue_ddt()  
+------------------+  
     |  
    \|/  
+---------------+  
| 设备/驱动注册    | <-- platform_device, platform_driver  
+---------------+  

- 技术点描述

1. 内核空间

Linux内核是与用户空间相隔离的程序。内核有自己独立的一片内存区域与用户程序隔离,有点像REE和TEE的关系。
当用户程序需要调用内核程序时,只能通过规定好的一些接口函数(系统调用)来使用内核的资源。内核程序拥有很高的权限可以控制硬件、调度和限制用户程序等。同时内核程序无法使用C语言标准库,所以所有的Linux内核程序调用的头文件都是以linux或asm开头的头文件。而且内核开发约定俗成的规则是,尖括号的include调用Linux内部头文件,双引号的include调用用户自定义的头文件。

#include <linux/device.h>
#include <asm/uaccess.h>
#include "smx.h"

当用户空间的程序通过系统调用,试图使用内核模块(内核程序)时,需要将传递的参数,通过copy_from_user()之类的函数,完整的*复制*到内核的内存,再由内核程序处理。内核程序运行完毕后,再通过copy_to_user()的方法,将结果复制给用户程序。
内核程序的编写和用户程序类似,从语法上完全遵循C语言,但有一些特定的宏关键字,来标记所使用变量/函数的信息。
内核程序的编译既可以和内核一同整体编译,也可以通过模块编译的方式,在Makefile中指定生成modules,于是GCC在生成*.o之后,会将其进一步生成*.mod.o*.ko。ko文件可以通过 insmod/rmmod/modprobe 等方式加载。通过printk打印的内核输出信息,在通过dmesg可以读到。

一个内核模块里的函数可以通过 EXPORT_SYMBOL(module_name) 的方式做外部声明,使之被其他的内核模块调用。同一个内核模块不同C文件内的函数,可以和C语言一样用头文件声明。

由于内核与用户空间的割裂,当试图从用户空间访问内核程序时(比如调用smx驱动做计算),需要接口模块。该模块跨接在内核和用户态之间,在用户态和内核态均需要做好相应的打包和解析。目前对于SMx,我们使用Socket的方法,在Linux源里面有af_alg的模块为基础作为参考。
af_alg的目的是将内核的crypto接口通过socket通道开放给用户空间。届时只要在用户空间调用af_alg的用户接口,传递参数,就可以操纵crypto函数计算。不过对于本项目而言,crypto接口默认提供的配置选项较少,无法满足需求;而af_alg又是针对crypto接口的定制,所以必须修改af_alg的部分内容,使得调用crypto API进行计算时,可以从用户态传入更多的配置选项配合硬件。

2. Crypto API

Crypto API是Linux内核里的一套用于密码运算及相关计算的模块。Linux自身带有少量常用密码算法和随机数生成算法。Crypto API的主要用途是给应用层以下的协议栈提供较用户空间更为安全的密码计算服务,比如网络层或协议层的网络协议IPSec等。
此次的crypto API开发参考了Linux源中picoxcell和spacc的实现。crypto API按密码算法类型分为多种接口,包括随机数rng,分组加密blkcipher,杂凑hash以及需要鉴权的杂凑aead。aead是blkcipher和hash的组合形式,类似HMAC。blkcipher和hash还有同步和异步两种任务分配的方式,异步任务称为ablkcipher和ahash。
典型的crypto API包含request, transform和algorithm三个部分,代码中的简写分别是req, tfm和alg。

用于定义算法基本结构

struct crypto_alg {
    struct list_head cra_list;
    struct list_head cra_users;

    u32 cra_flags;
    unsigned int cra_blocksize;
    unsigned int cra_ctxsize;
    unsigned int cra_alignmask;

    int cra_priority;
    atomic_t cra_refcnt;

    char cra_name[CRYPTO_MAX_ALG_NAME];
    char cra_driver_name[CRYPTO_MAX_ALG_NAME];

    const struct crypto_type *cra_type;

    union {
        struct ablkcipher_alg ablkcipher;
        struct blkcipher_alg blkcipher;
        struct cipher_alg cipher;
        struct compress_alg compress;
    } cra_u;

    int (*cra_init)(struct crypto_tfm *tfm);
    void (*cra_exit)(struct crypto_tfm *tfm);
    void (*cra_destroy)(struct crypto_alg *alg);
    
    struct module *cra_module;
};

异步分组算法的结构

struct ablkcipher_alg {
    int (* setkey) (struct crypto_ablkcipher *tfm, const u8 *key,unsigned int keylen);
    int (* encrypt) (struct ablkcipher_request *req);
    int (* decrypt) (struct ablkcipher_request *req);
    int (* givencrypt) (struct skcipher_givcrypt_request *req);
    int (* givdecrypt) (struct skcipher_givcrypt_request *req);
    const char * geniv;
    unsigned int min_keysize;
    unsigned int max_keysize;
    unsigned int ivsize;
};

算法结构和crypto的连接件

struct crypto_ablkcipher {
    struct crypto_tfm base;
};

crypto框架的基本结构

struct crypto_tfm {

    u32 crt_flags;
    
    union {
        struct ablkcipher_tfm ablkcipher;
        struct blkcipher_tfm blkcipher;
        struct cipher_tfm cipher;
        struct compress_tfm compress;
    } crt_u;

    void (*exit)(struct crypto_tfm *tfm);
    
    struct crypto_alg *__crt_alg;        //把这个tfm和某种算法连接

    void *__crt_ctx[] CRYPTO_MINALIGN_ATTR;
};

异步分组算法的crypto结构

struct ablkcipher_tfm {
    int (*setkey)(struct crypto_ablkcipher *tfm, const u8 *key,
                  unsigned int keylen);
    int (*encrypt)(struct ablkcipher_request *req);
    int (*decrypt)(struct ablkcipher_request *req);

    struct crypto_ablkcipher *base;

    unsigned int ivsize;
    unsigned int reqsize;
};
/* 
//对比同步加密,区别主要在加解密计算不是通过req的任务包方式,而是直接计算
struct blkcipher_tfm {
    void *iv;
    int (*setkey)(struct crypto_tfm *tfm, const u8 *key,
              unsigned int keylen);
    int (*encrypt)(struct blkcipher_desc *desc, struct scatterlist *dst,
               struct scatterlist *src, unsigned int nbytes);
    int (*decrypt)(struct blkcipher_desc *desc, struct scatterlist *dst,
               struct scatterlist *src, unsigned int nbytes);
};
*/

异步计算所需的需求包(任务包)结构,包含scatterlist的数据

struct ablkcipher_request {
    struct crypto_async_request base;

    unsigned int nbytes;

    void *info;

    struct scatterlist *src;
    struct scatterlist *dst;

    void *__ctx[] CRYPTO_MINALIGN_ATTR;
};

异步任务包的结构

struct crypto_async_request {
    struct list_head list;
    crypto_completion_t complete;
    void *data;
    struct crypto_tfm *tfm;

    u32 flags;
};

3. SG-DDT映射

SG (Scatter-Gather list) 是用于软件处理的一种数据结构,其根本是由于在内存中无法(不愿意)划出一块足够数据块大小的连续空间,而使用链表在逻辑上形成连续的表现形式。在Linux中,SG list(scatterlist) 的分配和管理在OS中有一套完整的制度(SPAcc驱动宣称此功能在ARM Linux中会出现bug)。
Scatterlist chaining

struct scatterlist {
    unsigned long    page_link;
    unsigned int    offset;
    unsigned int    length;
    dma_addr_t    dma_address;
};

其中,page_link = page_addr | LSB_TRICKS。
page是内存所能分配的最小单位(NTFS为4KB)首地址,offset是在此page中的偏移,LSB_TRICKS是为了表达sglist的数据结构,在page_link的最低2位玩的一些配置信息的花样。为了方便寻址,page_link必须至少按32-bit对齐,也因此最低2位可以用来表达配置信息。
举例:
page_link的page内容指向page x的首地址,offset为page x中的偏移,length可以跨越多个page。

offs = offset_in_page(buf) 计算buf在该页中的偏移量
sg_set_page(struct scatterlist sg, struct page page, len, offset) 让一个sg条目指向一个页中offset偏移的位置

 page x       page x+1     page x+2         page x+n
\------------\------------\------...-------\------------>>> virtual addr (DMA memory)
^        ^                                    ^
|        |<------------ length -------------->|
|       buf
|        |
|<-offs->|

当数据使用sglist存储后,会形成一个表。OS会按照顺序依次读取这个表,此表在虚拟空间内连续。

+-------------+--------+-----+----------+
| page_link_1 | offset | len | dma_addr | 
+-------------+--------+-----+----------+
+-------------+--------+-----+----------+
| page_link_2 | offset | len | dma_addr | 
+-------------+--------+-----+----------+
+-------------+--------+-----+----------+
| page_link_3 | offset | len | dma_addr | 
+-------------+--------+-----+----------+

遍历sglist是通过辨认page_link的最后两位识别是否是最后一个sglist节点,如果不是,则按逻辑地址累加的方式,读下一个sglist条目内容。

/**
 * sg_next - return the next scatterlist entry in a list
 * @sg:     The current sg entry
 *
 * Description:
 *   Usually the next entry will be @sg@ + 1, but if this sg element is part
 *   of a chained scatterlist, it could jump to the start of a new
 *   scatterlist array.
 *
 **/
struct scatterlist *sg_next(struct scatterlist *sg)
{
    ...
    if (sg_is_last(sg))
        return NULL;

    sg++;
    if (unlikely(sg_is_chain(sg)))
        sg = sg_chain_ptr(sg);

    return sg;
}

DDT (Direct DMA Table/DMA Data Transfer) 是用于硬件DMA优化的一种实现。其表现形式和数据结构如下:

+-----+----------+  
| len | dma_addr |  <-- entry 1
+-----+----------+
| len | dma_addr |  <-- entry 2
+-----+----------+
| len | dma_addr |  <-- entry 3
+-----+----------+
| len | dma_addr |  <-- entry 4
+-----+----------+
| len | dma_addr |  ...
+-----+----------+
| len | dma_addr |  <-- entry N
+-----+----------+            

在驱动中,需要将scatterlist中的数据,按照硬件的要求,排布到DDT表格中。硬件要求,DDT表格包括该条entry的数据块长度,和其对应的dma地址。dma地址不同于虚拟地址和物理地址,是在总线视角看到的设备地址。与CPU视角的物理地址不同,DMA地址并非由virt_to_phys()等函数获得,而是在内核中申请一段专门的DMA空间,在此空间中建立DDT。硬件DMA可以访问这个表,并通过这个物理空间连续的表格,依次读取dma_addr和len,直接搬入硬件进行计算。

在此次驱动设计中,硬件接口只接受DDT的表格地址,即entry的首地址。因此在开始计算前,需要驱动程序将输入的数据转换成scatterlist(内核动态开辟空间),以匹配crypto API,同时将scatterlist的内容映射到DDT中。将DDT的entry首地址(src和dst)写给硬件寄存器。计算完毕后,从DDT的dst entry读取结果,转换成scatterlist,然后从crypto API的框架接口读出数据。

4. 异步任务

SMx的硬件支持异步加密计算,即传入参数和加密指令后,不需要等待计算完成,软件便可提交下一次计算需求。所有的计算需求(任务包)和计算结果(状态包)都在一个有限深度的FIFO中缓冲。当计算结果累积到一定程度后,可以一次取出所有计算数据。两个FIFO相通,当任务包的任务计算完成,状态FIFO自动增加一条状态。

+-------+                           +-------+
| REQ 8 |  <-- 任务包进栈           | RES 8 |  <-- 状态包出栈
+-------+                           +-------+
| REQ 7 |                           | RES 7 |
+-------+                           +-------+
| REQ 6 |                           | RES 6 |
+-------+ <-- CMD_FIFO_TH           +-------+ <-- STAT_FIFO_TH
| REQ 5 |                           | RES 5 |
+-------+                           +-------+
| REQ 4 |                           | RES 4 |
+-------+                           +-------+
| REQ 3 |                           | RES 3 |
+-------+                           +-------+
| REQ 2 |                           | RES 2 |
+-------+                           +-------+
| REQ 1 | ======= 计算 =========>>  | RES 1 |  <-- 状态包进栈
+-------+                           +-------+

为了达到异步计算的目的,需要对驱动的一些计算流程进行并发化考虑,包括:

  • crypto API中使用async框架
  • 对计算输入进行组织,形成“任务包”形式的独立输入
  • 对计算状态进行监视,async框架中有(*complete)的回调函数,用于触发中断后的处理(重置计时器,读取status等)
  • 利用CMD_FIFOSTAT_FIFO阈值关联的中断信号,对数据流量进行管理(CMD_FIFO数量下降到阈值,STAT_FIFO数量由阈值上升到阈值+1

5. 中断

6. 设备注册和驱动注册

Comments
Write a Comment
'