在linux内核中经常会遇到这样的问题:需要在内核或者驱动中做一个开关变量,通过在用户态控制开关变量的值,从而让内核识别并处理不同的工作。常见的情况是,需
要做一个日志开关,用户可以控制内核是否打印出需要的日志。本文将介绍三种不同的方法,在内核中做开关变量。
在 阐述之前,我们假定用户通过cat和 echo 命令进行开关变量的读写。比如用户态开关文件位于 /home/value 。通过 cat /home/value 读取开关变量的值,通过
echo 1 > /home/value 类似的命令写入开关变量的值。之所以要用这两个命令,当然是为了方便,我们不希望再写一个程序,通过open-write-read这样的套路去读
写开关 文件。但是echo 这个程序和 write是有区别的,估计它的内部会连环调用write ,所以在内核中对应的write调用函数返回值需要特殊对待,具体在下面再说。
1 利用字符设备
在原有代码的基础上,添加一个字符设备,通过字符设备的read/write调用来设置开关变量的值。相对而言,该方法是比较麻烦的,但是的确可行。代码贴出如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
static char console_char = '0';
struct cdev * console_dev = NULL;
int console_open(struct inode *inode, struct file *filp)
{
filp->private_data= console_dev;
printk(KERN_ALERT"module is opened \n");
return 0;
}
int console_close(struct inode *inode, struct file *filp)
{
printk(KERN_ALERT "module is close \n");
return 0;
}
/*驱动对应的读方法*/
static ssize_t console_read(struct file *filp, char__user *buf, size_t size, loff_t *ppos)
{
put_user(console_char,buf);
return 0;
}
/*驱动对应的写方法*/
static ssize_t console_write(struct file *filp, constchar __user *buf, size_t size, loff_t *ppos)
{
get_user(console_char, buf);
printk(KERN_ALERT "console_char is %c\n", console_char);
switch(console_char)
{
/*通过不同的用户输入字做不同的处理*/
case 'd':
printk(KERN_ALERT "dev major num is %d, minor num is %d\n",MAJOR(console_dev->dev), MINOR(console_dev->dev));
break;
default:
break;
}
/*由于是利用echo,而且只写一个变量,所以直接返回size,而不是实际的写入数*/
return size;
}
struct file_operations console_fops = {
.owner = THIS_MODULE,
.open = console_open,
.release = console_close,
.write = console_write,
.read = console_read,
};
/*创建一个字符设备console*/
static struct cdev * alloc_cdev(void)
{
int devno =0;
struct cdev* dev = cdev_alloc();
if(NULL !=dev)
{
dev->ops= &console_fops;
dev->owner= THIS_MODULE;
alloc_chrdev_region(&dev->dev,0, 1, "console");
devno= MKDEV(MAJOR(dev->dev),(MINOR(dev->dev)));
if(0> cdev_add(dev, devno, 1))
{
printk(KERN_ALERT"cdev_add failed\n");
}
printk(KERN_ALERT"dev major num is %d, minor num is %d\n", MAJOR(dev->dev),MINOR(dev->dev));
}
return dev;
}
static void free_cdev(void)
{
if(NULL !=console_dev)
{
cdev_del(console_dev);
unregister_chrdev_region(MKDEV(MAJOR(console_dev->dev),0),1);
}
}
static int console_init(void)
{
printk(KERN_ALERT "console, init\n");
console_dev= alloc_cdev();
return 0;
}
static void console_exit(void)
{
free_cdev();
printk(KERN_ALERT "goodbye ,console quit\n");
}
module_init(console_init);
module_exit(console_exit);
完成后,编译出来,通过mkmond /dev/console c id1 id 2 来创建一个用户态节点文件,其中id1 id2 是console字符设备的主次设备号。需要特别说明的是,
内核的write调用方法,只写了一个字符,但是却直接返回 请求的size数量,这是因为我们调用echo 来写入一个字符,无论用户输入多少字符,我们都只写入一个字
符,而且骗echo我们已经全部写入了请求的写入数。比如我们执行echo 1234 > /dev/console ,实际上此时只写入1,其他的234 都作废。如果我们返回实际的写入数
,那么echo 会连续一直调用write函数,这并不是我们想要的。
运行字符设备的效果如下:
通过例子看出,用户态通过echo 写入的值已经在内核中生效,如果便相当于在内核中做了一个开关变量,而且可以通过用户态进行控制。
2 利用 proc 文件系统。
相对字符设备而言,proc 文件系统是比较简单的。但是一般proc文件系统都是只读,这里我们需要创建一个可读可写的proc文件,以此来控制内核中的开关变量。代码比
较简单,如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
char flag = '0';
static int read_proc_fun(char *buf, char **start, off_toffset, int count, int *eof, void *data)
{
int n =sprintf(buf, "%c", flag);
printk(KERN_ALERT "read flag is %c\n", flag);
*eof = 1;
return n;
}
static int write_proc_fun(struct file *filp, const char*buffer,unsigned long count,void *data)
{
char c = '0';
if(get_user(c,(char *)buffer))
{
return-EFAULT;
}
else
{
flag = c;
}
/*根据用户对flag的不同,进而设置不同的开关变量*/
printk(KERN_ALERT "set flag is %c\n", flag);
return count;/*为了使用echo ,此处不可返回1 个,否则会再次发生写动作。实际上我们只写了一个字符,并没有写多个*/
}
static void create_proc(void)
{
structproc_dir_entry *entry = NULL;
entry =create_proc_entry("dev_proc", 0644, NULL);
if(entry)
{
entry->read_proc= read_proc_fun;
entry->write_proc = write_proc_fun;
}
}
static void del_proc(void)
{
remove_proc_entry("dev_proc", NULL);
}
static int proc_init(void)
{
printk(KERN_ALERT "proc, hello\n");
create_proc();
return 0;
}
static void proc_exit(void)
{
printk(KERN_ALERT "goodbye ,proc \n");
del_proc();
}
module_init(proc_init);
module_exit(proc_exit);
利用proc 文件系统相对是比较简单的,不知道大家注意没有,proc文件的读写看起来并不统一,因为读方法read_proc中的buf直接就是用户态的地址,可以用sprintf
进行写入;而写方法write_proc 却是内核态的地址,需要用get_user或者 copy_from_user之类的方法,这始终是不太妥。 由于历史原因,proc文件系统不推荐使
用。所以接下来我们使用第三种方法。
3 利用 sysfs 文件系统。
当然这里写的比较简单和直接,没有对 kobject 等设备模型做处理,有兴趣的可以自己看 linux 内核相关的书籍。代码如下:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
MODULE_LICENSE("GPL");
struct kobject * my_kobject = NULL;
struct attribute my_attr;
struct sysfs_ops my_sysfs_ops;
char flag = '0';
ssize_t sf_show(struct kobject *kobj, struct attribute*attr, char *buffer)
{
int count =sprintf(buffer,"%c",flag);
return count;
}
ssize_t sf_store(struct kobject *kobj, struct attribute*attr, const char *buffer, size_t size)
{
flag = buffer[0];
printk(KERN_ALERT "flag is %c\n", flag);
return size;
}
static void sysfs_release(struct kobject *kobj)
{
printk(KERN_ALERT "sysfs_release\n");
}
static struct kobj_type dynamic_kobj_ktype = {
.release = sysfs_release,
.sysfs_ops = &my_sysfs_ops,
//.default_attrs = (struct attribute **)attr_list, /*这个地方暂时不用写上,内核中说的不是太清楚*/
};
static void sysfs_create(void)
{
my_kobject =kobject_create_and_add("test_sysfs", NULL);
if(my_kobject)
{
printk(KERN_ALERT"my_kobject %p\n", my_kobject);
my_attr.name= "my_sys_fs";
my_attr.mode= S_IRUGO | S_IWUGO;
my_attr.owner = NULL;
my_sysfs_ops.show = sf_show;
my_sysfs_ops.store = sf_store;
my_kobject->ktype= &dynamic_kobj_ktype;
sysfs_create_file(my_kobject, &my_attr);
}
}
static int __init sysfile_init(void)
{
printk(KERN_ALERT "sysfile, hello\n");
sysfs_create();
return 0;
}
static void __exit sysfile_exit(void)
{
printk(KERN_ALERT "goodbye ,sysfile \n");
if(NULL !=my_kobject)
{
sysfs_remove_file(my_kobject, &my_attr);
kobject_del(my_kobject);
kobject_put(my_kobject);
my_kobject= NULL;
}
}
module_init(sysfile_init);
module_exit(sysfile_exit);
利用sysfs文件系统有两点值得注意,第一: 相对 字符设备和proc文件系统的读写方法而言,sysfs 文件系统的读写空间地址都直接是用户态地址,不需要通过
copy_to_user和 copy_from_user这类的转换。第二 :上述结构dynamic_kobj_ktype中的default_attrs字符并没有使用,linux内核设计和实现的作者并没有给出详
细的原理和用例,该字段的用法显得比较含糊。通过查看linux的源码发现,源码中的该字段也并没有使用,所以我也暂时将该字段空着。对一个简单的字符开关而言,
这样处理是没有多大问题的。但是如果对于比较复杂的块设备,该字段的原理还需要进一步详细分析。做一件事情,如果不知道它的原理,始终还是不够心安。