linux用户态与内核态通过字符设备交互

linux用户态与内核态通过字符设备交互

简述

Linux设备分为三类,字符设备、块设备、网络接口设备。字符设备只能一个字节一个字节读取,常见外设基本都是字符设备。块设备一般用于存储设备,一块一块的读取。网络设备,Linux将对网络通信抽象成一个设备,通过套接字对其进行操作。

对于字符设备的用户态与内核态交互,主要涉及到打开、读取、写入、关闭等操作。通过字符设备实现内核与用户程序的交互,设计实现一个内核态监控文件目录及文件复制拷贝的内核模块程序,其中字符设备交互时序图如下:
user_space kernel_space 发送监控目录信息(list) 回复监控目录信息已设置 user_space kernel_space

通信协议格式

shell 复制代码
[2bytes数据长度] + |2bytes目录路径数量| + |2bytes 长度| + |目录数据| + ... + |2bytes 长度| + |目录数据|

控制命令定义

c 复制代码
#include <linux/ioctl.h>

#define BASEMINOR 0
#define COUNT 5
#define NAME "ioctl_test"

#define IOCTL_TYPE 'k'

//定义无参的命令
#define IOCTL_NO_ARG _IO(IOCTL_TYPE, 1)

//用户空间向内核空间写
#define IOCTL_WRITE_INT _IOW(IOCTL_TYPE, 2,int)

//用户空间从内核空间读
#define IOCTL_READ_INT _IOR(IOCTL_TYPE, 3, int)

//用户空间向内核空间写
#define IOCTL_WRITE_STRING _IOW(IOCTL_TYPE, 4,char*)

//用户空间从内核空间读
#define IOCTL_READ_STRING _IOR(IOCTL_TYPE, 5, char*)


#define IOCTL_MAXNR 5

上述命令实现了用户态向内核态写入、读取int型或string类型的数据,定义控制命令个数为5

用户态程序

c 复制代码
#include <stdio.h>
#include <string.h>
#include <linux/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include "cmd.h"
enum arg_type{
	ARG_INT,
	ARG_STRING
};
union data{
		int integer;
		char string[255];
	};

struct arg_node{
	int type; //字符串类型
	union data arg_data;
	struct arg_node*next;
};
void insert_node(struct arg_node**head, struct arg_node * item ){
		if(item==NULL)
		{
			printf("待插入节点指针为空\n");
			return ;
		}
		if(*head == NULL){
			*head = item;
			printf("节点指针赋值,%p\n",*head);
		}
		else{
			struct arg_node *current = *head;
			while(current->next != NULL){
				current = current->next;
			}
			current->next = item;
		}
	}

//参数格式:user_ipc -int 200 -string "12324154"
int main(int argc, char *argv[])
{
	if(argc<2 || argc%2==0)
	{
		printf("参数个数不匹配\n");
		return -1;
	}
	int fd = 0;
	int arg = 0;
	
	fd = open("/dev/ioctl_test", O_RDWR);
	if(fd < 0){
		printf("open memdev0 failed!\n");
		return -1;
	}
	if(ioctl(fd, IOCTL_NO_ARG, &arg) < 0){
		printf("----打印命令传输失败----\n");
		return -1;
	}
	
	unsigned char *protocol_body = NULL;
	int init_length = 5;
	int realloc_length = 10;
	int len_tag_bytes = 2;
	protocol_body = calloc(init_length, sizeof(unsigned char )*init_length);

	int index = 4;
	int num_of_dirs = 0;
	struct arg_node *p_head = NULL;
	int i=0;
	for(i=1; i<argc; i=i+2){
		if(strcmp(argv[i],"-int") == 0){
					struct arg_node*p_item = malloc(sizeof(struct arg_node));
					p_item->next = NULL;
					p_item->type = ARG_INT;
					p_item->arg_data.integer = atoi(argv[i+1]);
					insert_node(&p_head, p_item);
					printf("插入int类型,值: %d \n",p_item->arg_data.integer);
					if(p_head==NULL)
						printf("链表头指针为空\n");
		}
		else if(strcmp(argv[i], "-string") == 0){
			struct arg_node *p_item = malloc(sizeof(struct arg_node));
			p_item->next = NULL;		
			p_item->type = ARG_STRING;
			memcpy(p_item->arg_data.string, argv[i+1],strlen(argv[i+1]));
		    insert_node(&p_head, p_item);
			printf("插入string类型,值: %s \n",p_item->arg_data.string);

			//插入值组装协议数据包[2bytes数据长度] + [2bytes 字符串数量] +[2bytes长度] + [目录绝对路径] ... + [2bytes长度] + [目录绝对路径]
			int length = strlen(argv[i+1]);
			if((index+len_tag_bytes+length) > init_length) //空间不够,再分配
			{
				realloc_length = length + len_tag_bytes + 1; //计算再分配字节,多分配1个字节,作为结束null
				protocol_body = realloc(protocol_body, sizeof(unsigned char)*(init_length + realloc_length));
				if(!protocol_body){
					printf("再分配空间失败\n");
					exit(-1);
				}
				memset(protocol_body+index, 0, sizeof(unsigned char)*(init_length + realloc_length)); //初始化再分配空间为零
				init_length += realloc_length;
				printf("新分配空间成功,新分配空间字节大小 %d,总空间大小 %d\n",realloc_length, init_length);

			}
			protocol_body[index] = length / 256 ;
			protocol_body[index + 1] = length % 256;
			index = index + 2;
			memcpy(protocol_body + index, argv[i+1],length);
			index = index + length;
			num_of_dirs++;
		}
	}
	index = index -2;
	protocol_body[0] = index / 256;
	protocol_body[1] = index % 256;
	protocol_body[2] = num_of_dirs /256;
	protocol_body[3] = num_of_dirs %256;

	printf("组包数据:%d\n",index);
	for(i=0; i<index+2; i++){
		printf("%02x ",protocol_body[i]);

	}
	printf("\n");
	//内核交互 -- 字符设备
	if(ioctl(fd, IOCTL_WRITE_STRING, protocol_body)<0){
	    printf("----用户态向内核写入字符串数据失败----\n");
	    return -1;
	 }
	 char recv[256]={0};
	 if(ioctl(fd,IOCTL_READ_STRING,recv)<0){
	    printf("----用户态从内核态读取字符串数据失败----\n");
	    return -1;
	}
	printf("从内核态读取数据:%s\n",recv);
	//释放申请内存
	free(protocol_body);
	protocol_body = NULL;

	close(fd);
	return 0;
}

上述代码实现把多个int或者char*类型的数据插入链表中,但是实际使用中,这个链表比没有用,和用户态交互,我只使用了string类型的数据,再把数据存入到protocol_body中,通过控制命令IOCTL_WRITE_STRING,实现把protocol_body写入到字符设备,供内核模块读取,同时内核模块返回一个随机数。

编译命令

shell 复制代码
gcc -o user_ipc user_ipc.c

内核模块

c 复制代码
//msg_recv_send.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/ioctl.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/unistd.h>
#include <linux/random.h>
#include "cmd.h"
#include "ctl_data.h"



dev_t dev_num;
struct cdev *cdevp = NULL;

/*
struct dir_node{
        int length; //长度
        char *dir_s; //目录字符串
        struct list_head list; //链表
};
*/
LIST_HEAD(msg_list_head);

//处理
int handle_recv_msg(char *msg, int len){
	int ret = 0;
	int dir_index=0;
	//清空链表
	struct dir_node *entry, *tmp;
	
	list_for_each_entry_safe(entry, tmp, &msg_list_head,list){
		list_del(&entry->list);
		kfree(entry->dir_s);
		kfree(entry);
	}
	//解析数据
	int dir_length = 0;
	int num_of_dirs = 0;
	int char_index = 2;
	num_of_dirs = msg[0]<<8 | msg[1];
	for(dir_index=0; dir_index<num_of_dirs; dir_index++){
		dir_length = msg[char_index]<<8 | msg[char_index+1];
		char_index = char_index + 2;
		struct dir_node * new_node = kmalloc(sizeof(struct dir_node),GFP_KERNEL);
		new_node->dir_s = kmalloc(sizeof(char)*(dir_length+1),GFP_KERNEL);
		memset(new_node->dir_s, 0, dir_length+1);
		new_node->length = dir_length;
		INIT_LIST_HEAD(&new_node->list);
		memcpy(new_node->dir_s, msg+char_index, dir_length);
		char_index = char_index + dir_length;
		list_add_tail(&new_node->list, &msg_list_head);
	}
	//遍历列表
	list_for_each_entry(entry, &msg_list_head, list){
		printk(KERN_INFO "接收数据:%s\n",entry->dir_s);
	}	
	return ret;
}
static long my_ioctl(struct file * filp, unsigned int cmd, unsigned long arg){
	long ret = 0;
	int err = 0;
	int ioarg = 0;
	char kernel_buffer[256];
	unsigned int random_value = 0;		
	if(_IOC_TYPE(cmd) != IOCTL_TYPE){
		return -EINVAL;
	}
	if(_IOC_NR(cmd) > IOCTL_MAXNR){
		return -EINVAL;
	}
	
	if(_IOC_DIR(cmd) & _IOC_READ){
		err = !access_ok((void*)arg, _IOC_SIZE(cmd));
	}
	else if(_IOC_DIR(cmd) & _IOC_WRITE){
		err = !access_ok((void*)arg, _IOC_SIZE(cmd));
	}

	if(err){
		return -EFAULT;
	}
	switch(cmd){
		case IOCTL_NO_ARG:
			printk(KERN_INFO "print not arg cmd\n");
			break;
		case IOCTL_WRITE_INT:
			ret = __get_user(ioarg, (int*)arg);
			printk(KERN_INFO "get data from user space is :%d\n", ioarg);
			break;
		case IOCTL_READ_INT:
			ioarg = 1101;
			ret = __put_user(ioarg, (int *)arg);
			break;
		case IOCTL_WRITE_STRING:
			memset(kernel_buffer, 0, sizeof(kernel_buffer));
			unsigned char len[3]={0};

			ret = copy_from_user(len, (char*)arg, 2);
			int recv_len = 0;
			recv_len = len[0]*256 + len[1];
			
			printk(KERN_INFO "用户态写入的数据长度 %d",len[0]*256+len[1]);
			char *recv_buffer = kmalloc(sizeof(char)*recv_len,GFP_KERNEL);
			ret = copy_from_user(recv_buffer, (unsigned char*)(arg+2), recv_len);
			if(ret!=0){
				printk(KERN_INFO "从用户态拷贝数据失败,失败字节数 %d\n",ret);
			}
			printk(KERN_INFO "get data from user space is :%*ph\n",recv_len, recv_buffer);
					
			
			//处理接收到的字符串
			handle_recv_msg(recv_buffer, recv_len);
			kfree(recv_buffer);
			break;
		case IOCTL_READ_STRING:
			//memset(random_value, 0, sizeof(random_value));
			memset(kernel_buffer, 0, sizeof(kernel_buffer));
			random_value = get_random_int();
			snprintf(kernel_buffer, sizeof(kernel_buffer),"返回随机字符串数值:%u",random_value);
			printk(KERN_INFO "kern_buffer : %s\n",kernel_buffer);
			ret = copy_to_user((char *)arg,kernel_buffer,sizeof(kernel_buffer));
			if(ret == 0){
				printk(KERN_INFO "写文本字符到用户态成功,[%s]。\n",(char*)arg);
			}
			else{
				printk(KERN_INFO "写文本字符到用户态失败,未写入字节数 %d。\n",ret);
			}
			break;
		default:
			return -EINVAL;
	}
	return ret;
}

static const struct file_operations fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = my_ioctl
};

int __init ioctl_init(void ){
	int ret ;
	ret = alloc_chrdev_region(&dev_num, BASEMINOR, COUNT, NAME);
	if(ret < 0){
		printk(KERN_ERR "alloc_chrdev_region failed...\n");
		goto err1;
	}
	printk(KERN_INFO, "major = %d\n",MAJOR(dev_num));
    cdevp = cdev_alloc();
	if(NULL == cdevp){
		printk(KERN_ERR "cdev_alloc failed...\n");
		ret = -ENOMEM;
		goto err2;
	}
	cdev_init(cdevp, &fops);
	ret = cdev_add(cdevp, dev_num, COUNT);
	if(ret < 0){
		printk(KERN_INFO "cdev_add failed...\n");
		goto err2;
	}
	printk(KERN_INFO "------init completely\n");
	return 0;
err2:
		unregister_chrdev_region(dev_num, COUNT);
err1:
	return ret;
}

void __exit ioctl_exit(void){
	cdev_del(cdevp);
	unregister_chrdev_region(dev_num, COUNT);
	printk(KERN_INFO "exit success.");
}

上述代码中alloc_chrdev_region分配一个名为NAME的字符设备,

c 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
//dev 字符设备存储的指针,高12位是主设备号,低20位是从设备号
//baseminor是从设备号
//count 请求的设备号数量
//name  设备名

内核模块主文件

shell 复制代码
//file_shield.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/unistd.h>
#include <asm/ptrace.h>
#include <linux/kallsyms.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include <linux/cred.h>
#include "hook_func.h"
#include "ctl_data.h"
#include "msg_recv_send.h"

MODULE_LICENSE("GPL");

//增加字符设备处理逻辑代码

static int __init file_shield_init(void){
	int ret = 0;
	printk(KERN_INFO "init completly");
	//创建字符设备
	ioctl_init();
	printk(KERN_INFO "模块已加载\n");
	return ret;
}
static void __exit file_shield_exit(void){
	//卸载字符设备
	ioctl_exit();
	printk(KERN_INFO "模块已卸载\n");
}
module_init(file_shield_init);
module_exit(file_shield_exit);

内核模块Makefile

shell 复制代码
KERNELDIR:=/lib/modules/$(shell uname -r)/build
EXTRA_CFLAGS +=-O1

PWD = $(shell pwd)

obj-m +=file_hook.o
file_hook-objs:=file_shield.o msg_recv_send.o 

all:
	make -C $(KERNELDIR) M=$(PWD) modules
clean:
	make -C $(KERNELDIR) M=$(PWD) clean

编译

shell 复制代码
sudo make

输出

 LD [M]  /home/admin01/file-shield/file_hook.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/admin01/file-shield/file_hook.mod.o
  LD [M]  /home/admin01/file-shield/file_hook.ko
make[1]: 离开目录"/usr/src/linux-headers-5.4.18-53-generic"

设备节点文件

在Linux系统中,设备节点文件是一种用于与设备进行交互的接口。这些设备节点文件通常位于/dev目录下。在Linux系统中,设备节点文件是一种用于与设备进行交互的接口。这些设备节点文件通常位于/dev目录下。

设备节点文件是Linux中的一种特殊文件,用于与设备进行通信。它们允许用户空间程序通过标准的文件I/O操作(如打开、读取、写入、关闭)来与设备进行交互。在/dev目录下的每个设备节点文件都对应一个特定的设备或设备类。

在内核模块中注册字符设备时,通常使用cdev_add函数,它会告诉内核创建相应的设备节点文件。这些设备节点文件将在/dev目录下动态创建,以便用户空间程序能够访问注册的设备。

例如,如果你的设备被命名为my_device,在/dev目录下将创建一个名为my_device的设备节点文件。用户空间程序可以通过打开/dev/my_device来访问你的设备。

需要手动创建一个设备节点文件

shell 复制代码
sudo mknod /dev/ioctl_test c 240 0

加载内核模块

shell 复制代码
sudo insmod path/file_hook.ko

卸载内核模块

shell 复制代码
sudo rmmod file_hook

测试

用户态程序发送接收

内核模块发送与接收

相关推荐
vip45125 分钟前
Linux 经典面试八股文
linux
大霞上仙27 分钟前
Ubuntu系统电脑没有WiFi适配器
linux·运维·电脑
孤客网络科技工作室2 小时前
VMware 虚拟机使用教程及 Kali Linux 安装指南
linux·虚拟机·kali linux
不收藏找不到我2 小时前
浏览器交互事件汇总
前端·交互
颇有几分姿色2 小时前
深入理解 Linux 内存管理:free 命令详解
linux·运维·服务器
AndyFrank3 小时前
mac crontab 不能使用问题简记
linux·运维·macos
筱源源3 小时前
Kafka-linux环境部署
linux·kafka
算法与编程之美4 小时前
文件的写入与读取
linux·运维·服务器
xianwu5434 小时前
反向代理模块
linux·开发语言·网络·git
Amelio_Ming4 小时前
Permissions 0755 for ‘/etc/ssh/ssh_host_rsa_key‘ are too open.问题解决
linux·运维·ssh