[TOC]

在进行实验之前,查阅相关Makefile编写教程,链接如下: Makefile由浅入深–教程、干货

部分常用的预定义变量记录如下:

$* 不包含扩展名的目标文件名称 
$+ 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$< 第一个依赖文件的名称 
$? 所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚
$@ 目标的完整名称 ****
$^ 所有的依赖文件,以空格分开,不包含重复的依赖文件
$% 如果目标是归档成员,则该变量表示目标的归档成员名称

注意:目标文件,表示最终生成的可执行文件;依赖文件是目标文件生成所需的文件。它们是目标文件的输入,通常是源代码、配置文件或脚本等。

如以下例子:

target: dep1.o dep2.o
    echo $@
    echo $^
    echo $+
    echo $<
    echo $?

运行 make 时:

$@ → target $^ → dep1.o dep2.o (去重)
$+ → dep1.o dep2.o (可以重复) $< → dep1.o (第一个依赖文件)
$? → 如果 dep1.o 和 dep2.o 中有更新的,列出更新的依赖文件。

V2.6动态模块示例加载

考虑到在阅读课件ppt以及实验指导书后,对动态模块仍有很多疑惑,如:

  • 动态模块源代码存放位置是否有要求?
    • 没有要求。动态模块源代码应与Makefile文件在同一个文件夹下
  • 如何开始动态模块源代码的编写?
    • 在任意位置编写动态模块源代码及Makefile文件
  • 如何编译动态模块?
    • 编写Makefile文件编译
  • 如何查看当前主机linux可用动态模块编译版本?
    • 使用uname -r查看当前版本
    • 且当前版本的动态模块机制与 2.6 及其后的版本保持一致。

使用chatgpt询问后,部分疑惑解答如上,详细问答可查看以下链接: chatgpt询问动态模块疑惑

使用相关命令查看本地主机版本,结果如下:

[root@kp-test01 exper3]# uname -r
4.19.90-2110.8.0.0119.oe1.aarch64

由chat对话可知,该版本Linux内核使用动态模块规则与2.6版本保持一致。使用ppt提供的v2.6动态模块示例,源代码如下:

#include <linux/init.h>                                            /*必须要包含的头文件*/
#include <linux/kernel.h>
#include <linux/module.h>                                    /*必须要包含的头文件*/
static int mymodule_init(void)                                //模块初始化函数
{
  printk("hello,my module wored! \n");                    /*输出信息到内核日志*/
  return 0;
}
static void mymodule_exit(void) //模块清理函数
{ 
  printk("goodbye,unloading my module.\n");         /*输出信息到内核日志*/
}  
module_init(mymodule_init);                                //注册初始化函数
module_exit(mymodule_exit);                              //注册清理函数
MODULE_LICENSE("GPL");                              //模块许可声明

所用Makefile文件为:

ifneq ($(KERNELRELEASE),)
obj-m := mymodules.o       #obj-m指编译成外部模块
else
KERNELDIR := /lib/modules/$(shell uname -r)/build  #定义一个变量,指向内核目录
PWD := $(shell pwd)
 modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules  #编译内核模块
 endif

使用make命令编译结果如下:

[root@kp-test01 v26]# make
make -C /lib/modules/4.19.90-2110.8.0.0119.oe1.aarch64/build   M=/root/exper3/v26 modules  #编译内核模块
make[1]: Entering directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'
  CC [M]  /root/exper3/v26/mymodules.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/exper3/v26/mymodules.mod.o
  LD [M]  /root/exper3/v26/mymodules.ko
make[1]: Leaving directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'

可见编译结果正常且符合预期。

加载及查看模块的结果如下:

[root@kp-test01 v26]# insmod mymodules.ko 
[root@kp-test01 v26]# lsmod
Module                  Size  Used by
mymodules             262144  0
nf_log_ipv4           262144  1
nf_log_common         262144  1 nf_log_ipv4
xt_LOG                262144  1
ipt_REJECT            262144  1
nf_reject_ipv4        262144  1 ipt_REJECT
xt_set                262144  2
ip_set_hash_net       262144  1

可见模块正确加载

卸载模块:

[root@kp-test01 v26]# rmmod mymodules
[root@kp-test01 v26]# lsmod
Module                  Size  Used by
nf_log_ipv4           262144  1
nf_log_common         262144  1 nf_log_ipv4
xt_LOG                262144  1
ipt_REJECT            262144  1
nf_reject_ipv4        262144  1 ipt_REJECT
xt_set                262144  2
ip_set_hash_net       262144  1
ip_set                262144  2 xt_set,ip_set_hash_net

使用dmesg命令查看日志结果如下:

[ 6833.222562] mymodules: loading out-of-tree module taints kernel.
[ 6833.223120] mymodules: module verification failed: signature and/or required key missing - tainting kernel
[ 6833.224461] hello,my module wored! 
[ 6957.489659] goodbye,unloading my module.
[ 7155.850430] hello,my module wored! 
[ 7235.433122] goodbye,unloading my module.

成功输出源代码中的信息,示例被成功运行。

系统调用的篡改

用于系统调用篡改的动态模块源代码为:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
//original,syscall 78 function: gettimeofday
// new syscall 78 function: print "No 78 syscall has changed to hello" and return a+b
#define sys_No 78 

unsigned long old_sys_call_func;  
unsigned long p_sys_call_table=0xc0361860; // find in  /boot/System.map-'uname -r'

asmlinkage int hello(int a,int b) //new function
{
    printk("No 78  syscall has changed to hello");
    return a+b;
}
void modify_syscall(void)                                                        
{                                                                            
    unsigned long *sys_call_addr;                                            
    sys_call_addr=(unsigned long *)(p_sys_call_table+sys_No*4);   
    old_sys_call_func=*(sys_call_addr);                                      
    *(sys_call_addr)=(unsigned long)&hello;                //  point to new function
}                                                                              
void restore_syscall(void)                                                         
{                                                                              
    unsigned long *sys_call_addr;                                              
    sys_call_addr=(unsigned long *)(p_sys_call_table+sys_No*4); 
    *(sys_call_addr)=old_sys_call_func;                // point to original function
}   
static int mymodule_init(void)
{
  modify_syscall();
  return 0;
}
static void mymodule_exit(void)
{ 
  restore_syscall();
}  
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");

使用的测试程序如下:

#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
int main()
{
    struct  timeval    tv; 
    syscall(78,&tv,NULL); //before modify  syscall 78 :gettimeofday
    printf("tv_sec:%d\n",tv.tv_sec);
    printf("tv_usec:%d\n",tv.tv_usec);
    return 0;
}
modify_new_syscall.c
#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
int main()
{ 
    int ret=syscall(78,10,20); //after modify syscall 78
    printf("%d\n",ret);
    return 0;
}

这两个测试程序都为应用程序。 编译动态模块的Makefile文件如下:

ifneq ($(KERNELRELEASE),)
obj-m := modify_syscall.o       #obj-m指编译成外部模块
else
KERNELDIR := /lib/modules/$(shell uname -r)/build  #定义一个变量,指向内核目录
PWD := $(shell pwd)
 modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules  #编译内核模块
 endif

编译后结果如下:

[root@kp-test01 modify_syscall]# make
make -C /lib/modules/4.19.90-2110.8.0.0119.oe1.aarch64/build   M=/root/exper3/modify_syscall modules  #编译内核模块
make[1]: Entering directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'
  CC [M]  /root/exper3/modify_syscall/modify_syscall.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/exper3/modify_syscall/modify_syscall.mod.o
  LD [M]  /root/exper3/modify_syscall/modify_syscall.ko
make[1]: Leaving directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'
[root@kp-test01 modify_syscall]# ls
Makefile              modify_old_syscall.c  modify_syscall.ko     modify_syscall.mod.o  modules.order
modify_new_syscall.c  modify_syscall.c      modify_syscall.mod.c  modify_syscall.o      Module.symvers

在加载模块前后分别运行测试程序结果如下:

[root@kp-test01 modify_syscall]# gcc -o modify_old_syscall ./modify_old_syscall.c 
[root@kp-test01 modify_syscall]# gcc -o modify_new_syscall ./modify_new_syscall.c 
[root@kp-test01 modify_syscall]# ./modify_old_syscall 
tv_sec:4196032
tv_usec:0

加载模块后运行结果如下:

[root@kp-test01 modify_syscall]# insmod modify_syscall.ko 
Connection to 120.46.163.15 closed by remote host.
Connection to 120.46.163.15 closed.

~   88s

尝试多次,发现在加载模块时会自动断开连接。 动态模块加载断连 查阅上述资料后判断,可能有以下原因:

  1. 内核安全机制(如内核模块签名或防篡改)
    1. 现代 Linux 内核通常启用了以下机制来防止内核模块篡改:
      1. 内核模块签名验证: 默认要求模块经过签名。如果未正确签名,模块可能无法加载。
      2. 只读内存保护: 修改 sys_call_table 直接操作内核内存是受保护的。
      3. SELinux/AppArmor: 这些机制可能限制模块加载或内核内存修改。
  2. 地址错误或不兼容的内核版本 硬编码了 p_sys_call_table 的地址,但这个地址可能不适用于本地内核版本或编译配置。

考虑到之前加载未签名的v2.6动态模块示例未报错,猜测为地址错误。 查阅资料知: sys_call_table 地址通常被隐藏。动态查找是推荐方式,例如通过 kallsyms。 p_sys_call_table 地址可能导致访问非法内存,引发内核崩溃。 考虑使用动态获取 sys_call_table 的地址的方法:

#include <linux/kallsyms.h>
unsigned long *p_sys_call_table = NULL;

p_sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
if (!p_sys_call_table) {
    printk(KERN_ERR "Failed to find sys_call_table\n");
    return -1;
}

修改代码如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
//original,syscall 78 function: gettimeofday
// new syscall 78 function: print "No 78 syscall has changed to hello" and return a+b
#define sys_No 78 

unsigned long old_sys_call_func;  
// unsigned long p_sys_call_table=0xc0361860; // find in  /boot/System.map-'uname -r'

#include <linux/kallsyms.h>
unsigned long *p_sys_call_table = NULL;


asmlinkage int hello(int a,int b) //new function
{
    printk("No 78  syscall has changed to hello");
    return a+b;
}
void modify_syscall(void)                                                        
{                                                                            
    unsigned long *sys_call_addr;                                            
    sys_call_addr=(unsigned long *)(p_sys_call_table+sys_No);   
    old_sys_call_func=*(sys_call_addr);                                      
    *(sys_call_addr)=(unsigned long)&hello;                //  point to new function
}                                                                              
void restore_syscall(void)                                                         
{                                                                              
    unsigned long *sys_call_addr;                                              
    sys_call_addr=(unsigned long *)(p_sys_call_table+sys_No); 
    *(sys_call_addr)=old_sys_call_func;                // point to original function
}   
static int mymodule_init(void)
{
  p_sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
  if (!p_sys_call_table) {
      printk(KERN_ERR "Failed to find sys_call_table\n");
      return -1;
  }

  modify_syscall();
  return 0;
}
static void mymodule_exit(void)
{ 
  restore_syscall();
}  
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");

编译运行后,仍然在加载模块时断开连接,查看/boot/System.map-‘uname -r’文件中sys_call_table的地址,结果如下:

ffff000008af0670 r str__raw_syscalls__trace_system_name
ffff000008af0680 r __func__.41796
ffff000008af0698 R sys_call_table
ffff000008af0fc8 r __func__.38273

替换后重新编译,在加载模块时仍然断开连接,内核崩溃。之后查阅以下资料:

可知在修改系统调用时,需要先修改系统调用页表的可写性,关闭写保护。根据上述资料,考虑到实验环境中华为云openEuler操作系统为arm架构,最终在多次尝试下,使用update_mapping_prot函数修改整个.rodata segment为可写。该函数可从以下链接查看:
Linux ARM64 update_mapping_prot函数

修改后动态模块源代码如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/syscalls.h>
#include <asm/io.h>
#include <asm/unistd.h>  // 包含系统调用号等相关定义
#include <asm/pgtable.h>  // 为了访问页面表相关的函数和定义
#include <linux/uaccess.h>
#include <asm/paravirt.h>
#include <linux/kallsyms.h>

#define sys_No 78  // 需要修改的系统调用号

unsigned long old_sys_call_func;
unsigned long *p_sys_call_table = 0;  // 查找到的 sys_call_table 地址,依据 /boot/System.map 确定
// ffff000008af0698 R sys_call_table
// ffff000008af3a98 R a32_sys_call_table
void (*update_mapping_prot)(phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot);
// .rodata segment 区间
unsigned long start_rodata;
unsigned long init_begin;

#define section_size init_begin - start_rodata


//修改指定内核地址范围的内存属性为只读
static inline void protect_memory(void)
{
    update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
            section_size, PAGE_KERNEL_RO);
}

//修改指定内核地址范围的内存属性为可读可写等
static inline void unprotect_memory(void)
{
    update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
            section_size, PAGE_KERNEL);
}


// 新的系统调用函数
asmlinkage int hello(int a, int b) {
    printk("No 78 syscall has changed to hello\n");
    printk("a: %d, b: %d\n", a, b);
    return a + b;
}

// 修改系统调用
void modify_syscall(void) {
    update_mapping_prot = (void *)kallsyms_lookup_name("update_mapping_prot");

    start_rodata = (unsigned long)kallsyms_lookup_name("__start_rodata");
    init_begin = (unsigned long)kallsyms_lookup_name("__init_begin");

    p_sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
    printk("sys_call_addr: %p\n", p_sys_call_table);

    old_sys_call_func = (p_sys_call_table[sys_No]);  // 保存原始系统调用函数
    printk("old_sys_call_func: %lx\n", old_sys_call_func);

    unprotect_memory();
    p_sys_call_table[sys_No] = (unsigned long)&hello;
    protect_memory();

    printk("syscall 78 replaced successfully with hello function\n");
}

// 恢复原始的 sys_78 系统调用
void restore_syscall(void) {
    unprotect_memory();
    p_sys_call_table[sys_No] = old_sys_call_func;  // 恢复为原始的系统调用函数
    protect_memory();

    printk("syscall 78 restored to original\n");
}

// 模块初始化
static int mymodule_init(void) {
    printk("Module loading...\n");
    modify_syscall();
    return 0;
}

// 模块退出
static void mymodule_exit(void) {
    printk("Module unloading...\n");
    restore_syscall();
}

module_init(mymodule_init);
module_exit(mymodule_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("yuaay");
MODULE_DESCRIPTION("A simple syscall hook module for openEuler ARM");

注意:这里获得sys_call_table的地址时,使用的是kallsyms_lookup_name,这是因为System.map中显示的是内核编译时的静态地址,在运行时地址可能经过了动态调整,直接使用会访问错误的内存地址,导致内核崩溃。

此时重新编译代码加载模块,成功加载:

[root@kp-test01 modify_syscall]# make
make -C /lib/modules/4.19.90-2110.8.0.0119.oe1.aarch64/build   M=/root/exper3/modify_syscall modules  #编译内核模块
make[1]: Entering directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'
  CC [M]  /root/exper3/modify_syscall/modify_syscall.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/exper3/modify_syscall/modify_syscall.mod.o
  LD [M]  /root/exper3/modify_syscall/modify_syscall.ko
make[1]: Leaving directory '/usr/src/kernels/4.19.90-2110.8.0.0119.oe1.aarch64'
[root@kp-test01 modify_syscall]# insmod modify_syscall.ko 
[root@kp-test01 modify_syscall]# lsmod
Module                  Size  Used by
modify_syscall        262144  0
aes_ce_blk            262144  0
crypto_simd           262144  1 aes_ce_blk
cryptd                262144  1 crypto_simd

日志如下:

[ 3604.213062] Module loading...
[ 3604.228647] sys_call_addr: 00000000bca137c4
[ 3604.228941] old_sys_call_func: ffff0000083735e0
[ 3604.229251] syscall 78 replaced successfully with hello function
[ 3604.231748] No 78 syscall has changed to hello
[ 3604.232071] a: 537460416, b: 14352384
[ 3604.232340] No 78 syscall has changed to hello
[ 3604.232629] a: 537460416, b: 14352384
[ 3604.232867] No 78 syscall has changed to hello
[ 3604.233152] a: 537460416, b: 14352384
[ 3604.233383] No 78 syscall has changed to hello
[ 3604.233667] a: 537460416, b: 14352384
[ 3604.233916] No 78 syscall has changed to hello

其中tail的内容为:

[root@kp-test01 modify_syscall]# dmesg | tail
[ 3604.259986] No 78 syscall has changed to hello
[ 3604.260293] a: 537460416, b: 14352384
[ 3604.260540] No 78 syscall has changed to hello
[ 3604.260834] a: 537460416, b: 14352384
[ 3604.261076] No 78 syscall has changed to hello
[ 3604.261369] a: 537460416, b: 14352384
[ 3604.261616] No 78 syscall has changed to hello
[ 3604.261905] a: 537460416, b: 14352384
[ 3604.262161] No 78 syscall has changed to hello
[ 3604.262453] a: 537460416, b: 14352384

此时运行modify_new_syscall程序有结果:

[root@kp-test01 modify_syscall]# ./modify_new_syscall 
570293952
[root@kp-test01 modify_syscall]# ./modify_new_syscall 
572391104
[root@kp-test01 modify_syscall]# dmesg | tail
[ 3604.261076] No 78 syscall has changed to hello
[ 3604.261369] a: 537460416, b: 14352384
[ 3604.261616] No 78 syscall has changed to hello
[ 3604.261905] a: 537460416, b: 14352384
[ 3604.262161] No 78 syscall has changed to hello
[ 3604.262453] a: 537460416, b: 14352384
[ 3812.391724] No 78 syscall has changed to hello
[ 3812.392056] a: 555941568, b: 14352384
[ 3815.407503] No 78 syscall has changed to hello
[ 3815.407839] a: 558038720, b: 14352384

成功返回了a+b的值,说明加载内核正确。但值得注意的是,这里返回的a+b并不是由传入参数相加得到的。查阅相关资料可知,arm架构下syscall传入参数通过寄存器x0,x1传递,修改代码直接由寄存器值得到参数,代码如下:

// 新的系统调用函数
asmlinkage int hello(/* int *a, int *b */void) {
    int a = (int)current_pt_regs()->regs[0]; // 从寄存器 x0 获取参数 a
    int b = (int)current_pt_regs()->regs[1]; // 从寄存器 x1 获取参数 b
    
    printk("No 78 syscall has changed to hello\n");
    printk("a: %d, b: %d\n", a, b);
    
    return a + b; // 返回 a 和 b 的和
}

结果如下:

[root@openeuler-ultra test]# ./new
30
[root@openeuler-ultra test]# ./new
30
[root@openeuler-ultra test]# dmesg | tail 
[  728.237310] No 78 syscall has changed to hello
[  728.237601] a: -100, b: 940991640
[  728.237821] No 78 syscall has changed to hello
[  728.238108] a: -100, b: 940991640
[  728.238339] No 78 syscall has changed to hello
[  728.238630] a: -100, b: 940991640
[  729.208198] No 78 syscall has changed to hello
[  729.208538] a: 10, b: 20
[  730.538377] No 78 syscall has changed to hello
[  730.538709] a: -100, b: 738488808

正确。

配置模块签名

运行以下命令修改模块签名配置:

[root@kp-test01 modify_syscall]# make menuconfig
make: *** No rule to make target 'menuconfig'.  Stop.

查阅以下资料了解到该命令需要在完整的内核源码路径(例如 /usr/src/kernels/kernel-version)下执行。

更改目录后,安装ncurses 开发库,成功解决问题:

sudo yum install ncurses-devel bison flex elfutils-libelf-devel openssl-devel

下图为命令的图形化界面:

alt text

之后生成签名密钥对:

[root@kp-test01 4.19.90-2110.8.0.0119.oe1.aarch64]# scripts/sign-file sha256 ./signing_key.priv .
Usage: scripts/sign-file [-dp] <hash algo> <key> <x509> <module> [<dest>]
       scripts/sign-file -s <raw sig> <hash algo> <x509> <module> [<dest>] 

运行命令报错,查阅资料得知,需要先使用openssl生成私钥和自签名证书:

[root@kp-test01 4.19.90-2110.8.0.0119.oe1.aarch64]# openssl req -new -x509 -newkey rsa:2048 -keyout signing_key.priv -out signing_key.x509 -days 365 -nodes -subj "/CN=Custom Module Signing Key"
Generating a RSA private key
.....................................................................................................................................+++++
.................................................................................................+++++
writing new private key to 'signing_key.priv'
-----
[root@kp-test01 4.19.90-2110.8.0.0119.oe1.aarch64]# ls
arch   certs   Documentation  firmware  include  ipc      kernel  Makefile  Module.symvers  RPM-GPG-KEY-openEuler  scripts   signing_key.priv  sound       tools  virt
block  crypto  drivers        fs        init     Kconfig  lib     mm        net             samples                security  signing_key.x509  System.map  usr

之后重新执行命令:

[root@kp-test01 4.19.90-2110.8.0.0119.oe1.aarch64]# scripts/sign-file sha256 ./signing_key.priv ./signing_key.x509 ~/exper3/modify_syscall/modify_syscall.ko 
[root@kp-test01 4.19.90-2110.8.0.0119.oe1.aarch64]# cd ~/exper3/modify_syscall/
[root@kp-test01 modify_syscall]# modinfo ./modify_syscall.ko | grep -i signer
signer:         Custom Module Signing Key
[root@kp-test01 modify_syscall]# insmod modify_syscall.ko 
[root@kp-test01 modify_syscall]# lsmod
Module                  Size  Used by
modify_syscall        262144  0
aes_ce_blk            262144  0
crypto_simd           262144  1 aes_ce_blk
cryptd                262144  1 crypto_simd

字符设备

查阅以下资料:

可知要编写一个字符设备,要完成以下内容:

  • 实现相关操作函数:open, read, write, close, ioctl
  • 申请并分配设备号
  • 初始化设备文件
  • 绑定 file_operations 与操作函数的具体实现

考虑到本次实验中所用到的设备数量较少,选择不使用cdev,直接通过register_chrdev函数申请主设备号,并由该函数自动完成操作函数的绑定。对于设备文件,则通过定义的结构体来表示,在获得设备号后,通过kmalloc函数分配需要的内存空间。

代码编写如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>                                                        
#include <asm/uaccess.h>  
#include <linux/init.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <linux/device.h>


//相关操作函数声明
static int dev_open(struct inode *inode, struct file *filp);                                                      //打开字符设备
static ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);                      //读取字符设备(一个字节)
static ssize_t dev_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);        //写入字符设备(一个字节)
static loff_t dev_llseek(struct file *filp, loff_t offset, int whence);       //控制指针位置
static int dev_release(struct inode *inode, struct file *filp);                                                      //关闭字符设备
static int dev_ioctl(int fd, int cmd);  

MODULE_LICENSE("GPL");                                                       
#define MAJOR_NUM 290   
#define DEVICE_NAME "mydev"   

//定义字符设备的缓冲区的大小
#ifndef DEV_SIZE
#define DEV_SIZE 1024
#endif

//定义设备存储块结构体
struct Device
{
    /* data */
    char *data;
    unsigned long size;
};

//驱动子设备结构体数组的指针
struct Device *device;

//主设备号
static int major=MAJOR_NUM;

struct file_operations dev_fops ={    
    owner: THIS_MODULE,                                 
    read: dev_read,                                                       
    write: dev_write,
    open: dev_open,
    llseek: dev_llseek,
    release: dev_release,
    ioctl: dev_ioctl                                                      
};

static int init_mymodule(void)
{                                                
    int ret;                                                                  
    ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dev_fops);              
    if (ret)
    {                                                                    
        printk("dev register failure");                                        
    }else                                                                        
    {                                                                            
        printk("dev register success");                                        
    }    

    device = kmalloc(sizeof(struct Device), GFP_KERNEL);
    if(!device){
        //创建失败,释放设备号并返回
        result = -ENOMEM;
        unregister_chrdev(major, DEVICE_NAME);
        return result;
    }

    device->size = DEV_SIZE;
    device->data = kmalloc(DEV_SIZE, GFP_KERNEL);
    memset(device->data, 0, DEV_SIZE);


    //驱动模块启动成功
    printk("device init success\n");
    return 0;
}

static void cleanup_mymodule(void)
{                                            
    unregister_chrdev(MAJOR_NUM, "globalvar"); 
    //驱动模块卸载完毕
    printk(KERN_INFO "device exit success");
}  

int dev_open(inode *inode, file *filp)
{
    filp->private_data=device;
    return 0;
}

ssize_t dev_read(file *filp, char __user *buf, size_t size, loff_t *pos)
{
    unsigned long position=*pos;
    unsigned int count=size;
    int result;
    struct Device *dev=filp->private_data;

    //如果要求位置非法则直接退出
    if(position>=DEV_SIZE)
        return 0;
    //调整读取数据长度
    if(count > DEV_SIZE - position)
    {
        count = DEV_SIZE - position;
    }

    //传送数据给用户
    if(copy_to_user(buf, (void*)(dev->data + position), count)) {
        result = -EFAULT;
    } else {
        *pos += count;
        result = count;
        printk(KERN_INFO "read %d bytes from %lu\n", count, position);
    }

    return result;
}

ssize_t dev_write(file *filp, const char __user *buf, size_t size, loff_t *pos)
{
    unsigned long position = *pos;
    unsigned int count = size;
    int result = 0;
    struct Device *dev = filp->private_data;

    //确保写入安全
    if (position >= DEV_SIZE)
        return 0;
    if (count > DEV_SIZE - position)
    {
        count = DEV_SIZE - position;
    }
    //写入数据
    if (copy_from_user(dev->data + position, buf, count)) {
        result = -EFAULT;
    } else {
        *pos += count;
        result = count;
        printk(KERN_INFO "write %d bytes from %lu\n", count, position);
    }

    return result;
}

loff_t dev_llseek(file *filp, loff_t offset, int whence)
{
    loff_t newpos;

    //选择操作类型
    switch (whence) {
        //指定新位置
        case 0:
            newpos = offset;
            break;
        //后移指针
        case 1:
            newpos = filp->f_pos + offset;
            break;
        //反向指定新位置
        case 2:
            newpos = DEV_SIZE - 1 + offset;
            break;
        default:
        return -EINVAL;
    }
    if ((newpos < 0) || (newpos > DEV_SIZE))
        return -EINVAL;

    filp->f_pos = newpos;
    return newpos;
}

int dev_release(inode *inode, file *filp)
{
    return 0;
}

int dev_ioctl(int fd, int cmd)
{
    return 0;
}

module_init(init_mymodule);
module_exit(cleanup_mymodule);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yuaay");

考虑到以下问题:

  • struct file_operations 中的字段名需要显式初始化,通过形式: .read = dev_read。
  • ioctl 未被标准 struct file_operations 支持。
  • 在模块卸载时需要释放所有分配的内存。

修正代码如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>

// 相关操作函数声明
static int dev_open(struct inode *inode, struct file *filp);
static ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);
static ssize_t dev_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);
static loff_t dev_llseek(struct file *filp, loff_t offset, int whence);
static int dev_release(struct inode *inode, struct file *filp);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("yuaay");
#define MAJOR_NUM 290
#define DEVICE_NAME "mydev"
#define DEV_SIZE 1024

// 定义设备存储块结构体
struct Device {
    char *data;
    unsigned long size;
};

// 驱动子设备结构体指针
struct Device *device;

// 文件操作结构体
struct file_operations dev_fops = {
    .owner = THIS_MODULE,
    .read = dev_read,
    .write = dev_write,
    .open = dev_open,
    .llseek = dev_llseek,
    .release = dev_release
};

// 模块初始化函数
static int __init init_mymodule(void) {
    int result;

    // 注册字符设备
    result = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dev_fops);
    if (result < 0) {
        printk(KERN_ERR "Failed to register character device\n");
        return result;
    }

    // 分配设备结构体
    device = kmalloc(sizeof(struct Device), GFP_KERNEL);
    if (!device) {
        unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
        return -ENOMEM;
    }

    device->size = DEV_SIZE;
    device->data = kmalloc(DEV_SIZE, GFP_KERNEL);
    if (!device->data) {
        kfree(device);
        unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
        return -ENOMEM;
    }

    memset(device->data, 0, DEV_SIZE);
    printk(KERN_INFO "Device initialized successfully\n");
    return 0;
}

// 模块清理函数
static void __exit cleanup_mymodule(void) {
    if (device) {
        if (device->data)
            kfree(device->data);
        kfree(device);
    }
    unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
    printk(KERN_INFO "Device unregistered successfully\n");
}

static int dev_open(struct inode *inode, struct file *filp) {
    filp->private_data = device;
    return 0;
}

static ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
    struct Device *dev = filp->private_data;
    size_t to_read;
    int ret;

    if (*pos >= DEV_SIZE)
        return 0;

    to_read = min(size, (size_t)(DEV_SIZE - *pos));
    if (copy_to_user(buf, dev->data + *pos, to_read))
        return -EFAULT;

    *pos += to_read;
    return to_read;
}

static ssize_t dev_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
    struct Device *dev = filp->private_data;
    size_t to_write;

    if (*pos >= DEV_SIZE)
        return 0;

    to_write = min(size, (size_t)(DEV_SIZE - *pos));
    if (copy_from_user(dev->data + *pos, buf, to_write))
        return -EFAULT;

    *pos += to_write;
    return to_write;
}

static loff_t dev_llseek(struct file *filp, loff_t offset, int whence) {
    loff_t newpos;

    switch (whence) {
    case SEEK_SET:
        newpos = offset;
        break;
    case SEEK_CUR:
        newpos = filp->f_pos + offset;
        break;
    case SEEK_END:
        newpos = DEV_SIZE + offset;
        break;
    default:
        return -EINVAL;
    }

    if (newpos < 0 || newpos > DEV_SIZE)
        return -EINVAL;

    filp->f_pos = newpos;
    return newpos;
}

static int dev_release(struct inode *inode, struct file *filp) {
    return 0;
}

module_init(init_mymodule);
module_exit(cleanup_mymodule);

使用的Makefile文件如下:

obj-m += mydev.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译后执行以下命令:

# 加载模块
insmod mydev.ko 
# 查看日志信息
dmesg | tail
# 创建设备节点
mknod /dev/mydev c 290 0
# /dev/mydev 是设备文件路径
# 290 是主设备号(由代码 #define MAJOR_NUM 290 定义)
# 0 是次设备号,因为代码里程序只能管理一个子设备,故只需要为0即可
chmod 666 /dev/mydev
# 为设备节点赋予权限

执行结果如下:

[root@openeuler-arm dev]# insmod mydev.ko 
[root@openeuler-arm dev]# dmesg | tail -5
[11955.474934] mydev: loading out-of-tree module taints kernel.
[11955.475437] mydev: module verification failed: signature and/or required key missing - tainting kernel
[11955.476600] Device initialized successfully
[12335.485136] Device unregistered successfully
[12549.117348] Device initialized successfully
[root@openeuler-arm dev]# mknod /dev/mydev c 290 0
mknod: /dev/mydev: File exists
[root@openeuler-arm dev]# chmod 666 /dev/mydev

在这里因为已经执行过一次,故不需要再次创建设备节点

之后,通过终端执行以下操作验证设备操作:

[root@openeuler-arm dev]# echo "Hello, Device!" > /dev/mydev
[root@openeuler-arm dev]# cat /dev/mydev
Hello, Device!
[root@openeuler-arm dev]# echo "1234567890" > /dev/mydev
[root@openeuler-arm dev]# dd if=/dev/mydev bs=1 skip=5 count=5
678905+0 records in
5+0 records out
5 bytes copied, 3.9981e-05 s, 125 kB/s

可见数据的读取、写入、指针移动均正常。尝试在不同终端中读取、写入。 在第一个终端中写入,执行以下命令:

[root@openeuler-arm dev]# echo "Data from T1" > /dev/mydev

新建另一个终端读取,结果如下图: alt text 可见数据的读取正确。

将模块卸载,检查日志:

[root@openeuler-arm dev]# rmmod mydev
[root@openeuler-arm dev]# dmesg | tail
[    4.183435] systemd-journald[473]: Received client request to flush runtime journal.
[    6.611860] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
[    7.541671] cryptd: max_cpu_qlen set to 1000
[    9.894592] VFS: Open an exclusive opened block device for write vda2 [2137 resize2fs].
[11955.474934] mydev: loading out-of-tree module taints kernel.
[11955.475437] mydev: module verification failed: signature and/or required key missing - tainting kernel
[11955.476600] Device initialized successfully
[12335.485136] Device unregistered successfully

可见模块的卸载正常。

将模块的测试用代码来体现,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define DEVICE_PATH "/dev/mydev"
#define BUFFER_SIZE 256

void test_open_close() {
    int fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return;
    }
    printf("Device opened successfully\n");
    close(fd);
    printf("Device closed successfully\n");
}

void test_read() {
    int fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return;
    }

    char buffer[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);

    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read < 0) {
        perror("Failed to read from device");
    } else {
        printf("Read %zd bytes: %s\n", bytes_read, buffer);
    }

    close(fd);
}

void test_write() {
    int fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return;
    }

    const char *data = "Hello, kernel device!";
    ssize_t bytes_written = write(fd, data, strlen(data));
    if (bytes_written < 0) {
        perror("Failed to write to device");
    } else {
        printf("Wrote %zd bytes: %s\n", bytes_written, data);
    }

    close(fd);
}

void test_llseek() {
    int fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return;
    }

    // Seek to position 10
    off_t new_pos = lseek(fd, 10, SEEK_SET);
    if (new_pos < 0) {
        perror("Failed to seek");
    } else {
        printf("Seeked to position: %lld\n", (long long)new_pos);
        char buffer[BUFFER_SIZE];
        memset(buffer, 0, BUFFER_SIZE);

        ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
        if (bytes_read < 0) {
            perror("Failed to read from device");
        } else {
            printf("Read %zd bytes: %s\n", bytes_read, buffer);
        }
    }

    close(fd);
}

int main() {
    printf("Starting device tests...\n");

    printf("\nTesting open/close:\n");
    test_open_close();

    printf("\nTesting write:\n");
    test_write();

    printf("\nTesting read:\n");
    test_read();

    printf("\nTesting llseek:\n");
    test_llseek();

    printf("\nAll tests completed.\n");
    return 0;
}

运行结果如下:

[root@openeuler-arm dev]# gcc -o test_mydev ./test_mydev.c 
[root@openeuler-arm dev]# ./test_mydev 
Starting device tests...

Testing open/close:
Device opened successfully
Device closed successfully

Testing write:
Wrote 21 bytes: Hello, kernel device!

Testing read:
Read 256 bytes: Hello, kernel device!

Testing llseek:
Seeked to position: 10
Read 256 bytes: nel device!

All tests completed.
[root@openeuler-arm dev]# 

可见运行的结果符合预期。

聊天程序的设计

设计思路

考虑到在聊天程序中,数据并不适合连续的存储,相反,每一次的发送、读取都是针对一条消息而言的。故修改设备模块的文件结构,将一个device定义为如下结构体:

struct Message 
{
    pid_t sender_pid;    // 发送者进程号
    pid_t target_pid;    // 目标接收者进程号,0 表示群发
    char content[MAX_MSG_LEN]; // 消息内容
};

struct User
{
    pid_t pid;
    int head;
    int count;
};


struct MessageQueue 
{
    struct Message messages[MAX_MSG_COUNT];
    // int curr;         //下一个该写入的位置
    // int head;             // 队列头
    int tail;             // 队列尾
    spinlock_t lock;      // 自旋锁保护队列
    // int count;            // 队列中消息数量
    int users_num;      // 目前用户数量
    struct User users[USERS_MAX_NUM];

};

// 定义设备存储块结构体,使用消息队列定义
struct Device 
{
    struct MessageQueue queue;
};

其中,一个设备由消息队列,队列尾(同时指向下一个写入的位置),自旋锁,使用的进程数量,以及用于记录用户信息的数组组成。

此外,该聊天程序需要实现多对多的消息转发,每一个进程都需要读取其余进程发送的消息。也就是说,对于任何一个进程都需要记录他们对应的读取位置,从而实现在设备内一个消息可以被多个进程读取,而对于任意一个进程而言,任何一个消息同时又只读取一次。

这里考虑在设备结点文件中记录每个用户的信息,通过User结构体,记录当前进程的pid以及读取的位置、还没有读取的消息数量,之后在read函数中,遍历没有读取的消息,读取能够读取的信息。

此外,对于多个用户而言,读取、写入时还存在竞争与同步的问题。这个问题一方面可以在聊天程序客户端中使用自旋锁或者信号量解决,一方面也可以在设备模块中,在对应的写入和读取中使用自旋锁解决。这里选用第二种方式,同时由于自旋锁是被施加在写入、读取函数中,在客户端程序中就可以随意调用操作函数。

字符设备模块代码

完整的代码如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/spinlock.h>

// 相关操作函数声明
static int dev_open(struct inode *inode, struct file *filp);
static ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);
static ssize_t dev_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);
static loff_t dev_llseek(struct file *filp, loff_t offset, int whence);
static int dev_release(struct inode *inode, struct file *filp);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("yuaay");
#define MAJOR_NUM 290
#define DEVICE_NAME "mydev"
#define DEV_SIZE 1024

#define MAX_MSG_LEN 256
#define MAX_MSG_COUNT 64
#define USERS_MAX_NUM 6


struct Message 
{
    pid_t sender_pid;    // 发送者进程号
    pid_t target_pid;    // 目标接收者进程号,0 表示群发
    char content[MAX_MSG_LEN]; // 消息内容
};

struct User
{
    pid_t pid;
    int head;
    int count;
};


struct MessageQueue 
{
    struct Message messages[MAX_MSG_COUNT];
    // int curr;         //下一个该写入的位置
    // int head;             // 队列头
    int tail;             // 队列尾
    spinlock_t lock;      // 自旋锁保护队列
    // int count;            // 队列中消息数量
    int users_num;      // 目前用户数量
    struct User users[USERS_MAX_NUM];

};

// 定义设备存储块结构体,使用消息队列定义
struct Device 
{
    struct MessageQueue queue;
};

// 驱动子设备结构体指针
struct Device *device;

// 文件操作结构体
struct file_operations dev_fops = 
{
    .owner = THIS_MODULE,
    .read = dev_read,
    .write = dev_write,
    .open = dev_open,
    .llseek = dev_llseek,
    .release = dev_release
};

// 模块初始化函数
static int __init init_mymodule(void) 
{
    int result;

    // 注册字符设备
    result = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dev_fops);
    if (result < 0) 
    {
        printk(KERN_ERR "Failed to register character device\n");
        return result;
    }

    // 分配设备结构体
    device = kmalloc(sizeof(struct Device), GFP_KERNEL);
    if (!device) 
    {
        unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
        return -ENOMEM;
    }

    // 初始化消息队列
    spin_lock_init(&device->queue.lock);
    // device->queue.head = 0;
    device->queue.tail = 0;
    // device->queue.count = 0;
    // device->queue.curr = 0;
    device->queue.users_num = 0;

    printk(KERN_INFO "Device initialized successfully\n");
    return 0;
}

// 模块清理函数
static void __exit cleanup_mymodule(void) 
{
    if (device) 
    {
        kfree(device);
    }
    unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
    printk(KERN_INFO "Device unregistered successfully\n");
}

static int dev_open(struct inode *inode, struct file *filp) 
{
    spin_lock(&device->queue.lock);
    if (device->queue.users_num >= USERS_MAX_NUM)
    {
        printk("open: users max");
        return -ENOMEM;
    }
    filp->private_data = device;
    device->queue.users[device->queue.users_num].pid = current->pid + 1;
    device->queue.users[device->queue.users_num].head = 0;
    device->queue.users[device->queue.users_num].count = device->queue.tail;
    device->queue.users_num++;
    printk("users_num: %d, new_user_pid: %d, new_user_count: %d", device->queue.users_num, current->pid, device->queue.tail);
    spin_unlock(&device->queue.lock);

    return 0;
}

static ssize_t dev_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) 
{
    struct Device *dev = filp->private_data;
    struct MessageQueue *queue = &dev->queue;
    struct Message msg;
    size_t copy_size;
    int user_num = 0;
    pid_t pid = current->pid;
    // printk("read: pid: %d", pid);
    int i;
    int found = 0;

    spin_lock(&queue->lock);
    for (i = 0; i < queue->users_num; i++)
    {
        if (queue->users[i].pid == pid)
        {
            // printk("read: find the user: %d", pid);
            user_num = i;
            found = 1;
            break;
        }
    }
    if (!found)
    {
        spin_unlock(&queue->lock);
        // printk("read: no registered user");
        return 0;
    }
    queue->users[user_num].count = (queue->tail - queue->users[user_num].head + MAX_MSG_COUNT) % MAX_MSG_COUNT;
    // printk("read: user_num: %d", user_num);
    if (queue->users[user_num].count == 0) 
    {
        // printk("read: nomessage!");
        spin_unlock(&queue->lock);
        return 0;  // 队列为空
    }

    // 查找属于当前进程的消息
    found = 0;
    
    int index = (queue->users[user_num].head) % MAX_MSG_COUNT;
    if (queue->messages[index].target_pid == 0 ||  // 群发消息
        queue->messages[index].target_pid == pid )
    {  
        msg = queue->messages[index];
        queue->users[user_num].head = (queue->users[user_num].head + 1) % MAX_MSG_COUNT;
        // queue->users[user_num].count--;
        found = 1;
        printk("read: find the message! count: %d", queue->users[user_num].head);
    }
    else
    {
        queue->users[user_num].head = (queue->users[user_num].head + 1) % MAX_MSG_COUNT;
        // queue->users[user_num].count--;
    }
    spin_unlock(&queue->lock);

    if (!found) {
        return 0;  // 没有可读取的消息
        printk("read: current is not available message! count: %d", queue->users[user_num].count);
    }

    // 将消息内容复制到用户空间
    copy_size = min(size, sizeof(msg.content));
    if (copy_to_user(buf, &msg.content, copy_size))
        return -EFAULT;

    return copy_size;
}

static ssize_t dev_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) 
{
    struct Device *dev = filp->private_data;
    struct MessageQueue *queue = &dev->queue;
    struct Message msg;
    size_t copy_size;
    char temp[MAX_MSG_LEN];

    if (size > MAX_MSG_LEN)
        return -EINVAL;

    copy_size = min(size, sizeof(temp) - 1);
    if (copy_from_user(temp, buf, copy_size))
        return -EFAULT;

    temp[copy_size] = '\0';  // 确保消息是以 NULL 结尾的字符串

    // 初始化消息
    msg.sender_pid = current->pid;
    msg.target_pid = 0;  // 默认是群发

    // 检查是否是私聊消息
    if (temp[0] == '@') 
    {
        char *endptr;
        msg.target_pid = simple_strtol(temp + 1, &endptr, 10);  // 提取目标 PID
        if (*endptr != ' ' && *endptr != '\0') 
        {
            return -EINVAL;  // 如果格式错误,返回无效参数
        }
        // 消息内容跳过 "@pid "
        memmove(temp, endptr + 1, strlen(endptr + 1) + 1);
        printk("write: This is a '@' message!");
    }

    strncpy(msg.content, temp, MAX_MSG_LEN - 1);
    msg.content[MAX_MSG_LEN - 1] = '\0';

    // 加入消息队列
    spin_lock(&queue->lock);
    // 设计为循环队列,消息满了后重头开始
    // if (queue->count == MAX_MSG_COUNT) 
    // {
    //     spin_unlock(&queue->lock);
    //     return -ENOMEM;  // 队列已满
    // }

    queue->messages[queue->tail] = msg;
    queue->tail = (queue->tail + 1) % MAX_MSG_COUNT;
    printk("write: write complete!");
    printk("write: content: %s", msg.content);
    spin_unlock(&queue->lock);

    return size;
}

static loff_t dev_llseek(struct file *filp, loff_t offset, int whence) 
{
    loff_t newpos;

    switch (whence) {
    case SEEK_SET:
        newpos = offset;
        break;
    case SEEK_CUR:
        newpos = filp->f_pos + offset;
        break;
    case SEEK_END:
        newpos = DEV_SIZE + offset;
        break;
    default:
        return -EINVAL;
    }

    if (newpos < 0 || newpos > DEV_SIZE)
        return -EINVAL;

    filp->f_pos = newpos;
    return newpos;
}

static int dev_release(struct inode *inode, struct file *filp) 
{
    return 0;
}

module_init(init_mymodule);
module_exit(cleanup_mymodule);

聊天程序设计

由于在设备模块代码中已经实现了自旋锁,故在聊天程序只需要通过两个线程,一个负责读取并处理消息,一个负责用户写入消息的处理即可。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>

#define DEVICE_PATH "/dev/mydev"
#define MAX_MSG_LEN 256

// int last = 0; // last = 0表示上一行消息为读取,1表示上一行消息为写入
// struct Message 
// {
//     pid_t sender_pid;    // 发送者进程号
//     pid_t target_pid;    // 目标接收者进程号,0 表示群发
//     char content[MAX_MSG_LEN]; // 消息内容
// };

void *receive_messages(void *arg) {
    int fd = *(int *)arg;
    char buffer[MAX_MSG_LEN];
    // struct Message *buffer;
    ssize_t len;

    while (1) {
        len = read(fd, buffer, sizeof(buffer));
        if (len > 0) {
            buffer[len - 8] = '\0';  // 确保消息以 NULL 结尾
            // if (last)
            // {
            //     printf("\n[Received]: %s\n", buffer);
            // }
            // else
            // {
            //     printf("[Received]: %s\n", buffer);
            // }
            // last = 0;
            printf("[Received]: %s\n", buffer);
        } else if (len < 0) {
            perror("Error reading from device");
            break;
        }
    }
    return NULL;
}

int main() {
    int fd;
    char input[MAX_MSG_LEN];
    pthread_t receiver_thread;

    // 打开字符设备
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return EXIT_FAILURE;
    }

    // 启动接收线程
    if (pthread_create(&receiver_thread, NULL, receive_messages, &fd) != 0) {
        perror("Failed to create receiver thread");
        close(fd);
        return EXIT_FAILURE;
    }

    printf("Enter messages to send. Type 'exit' to quit. Current user: %d\n", getpid() + 1);
    while (1) {
        // printf("Enter message: ");
        // last = 1;
        if (fgets(input, sizeof(input), stdin) == NULL) {
            perror("Error reading input");
            break;
        }

        // 去掉换行符
        input[strcspn(input, "\n")] = '\0';

        if (strcmp(input, "exit") == 0) {
            break;
        }

        // 写入消息
        if (write(fd, input, strlen(input)) < 0) {
            perror("Error writing to device");
            break;
        }
    }

    // 关闭设备并退出
    close(fd);
    pthread_cancel(receiver_thread);
    pthread_join(receiver_thread, NULL);

    printf("Client exited.\n");
    return EXIT_SUCCESS;
}

运行结果

以下为三个进程的运行结果图: alt text

可见成功实现了多对多的对话,并且经过测试,通过@标识的私聊信息也正常发送。