Char device driver

Character device driver

Representation of major and minor number

  • In the kernel itself the bit range 0 — 19, that is, 20 bits are used for the minor number. This leaves 12 bits in the range 20 — 31 for the major number
  • When it is necessary to represent a dev_t externally, the 8 bits from the range 0 — 7 are used for the first part of the minor number, the next 12 bits (range 8 — 19) for the major number, and the last 12 bits (range 20 — 31) for the missing part of the minor number.

    The kernel provides the functions/macros listed below (and defined in <kdev_t.h>) to extract informa- tion from the u32 representation and convert between u32 and dev_t.
  1. MAJOR and MINOR extract the major and minor number, respectively, from a dev_t.

  2. MKDEV(major, minor) generates a dev_t type from the major and minor numbers.

  3. new_encode_dev converts dev_t to u32 in the external representation mentioned above.

  4. new_decode_dev converts the external representation to dev_t.

  5. old_encode_dev and old_decode_dev switches between a number of type u16, that is, the old representation, and the modern dev_t representation.

<kdev_t.h>
u16 old_encode_dev(dev_t dev); 
dev_t old_decode_dev(u16 val); 
u32 new_encode_dev(dev_t dev); 
dev_t new_decode_dev(u32 dev);

Registration

The Device Database

There is a special data structure to manage the all the block and character device, because…

  • Each character device is represented by an instance of struct cdev.
  • struct genhd is used to manage partitions of block devices and plays a role similar to that of cdev for character devices. This is reasonable since a block device without partitions can also be seen as a block device with a single, large partition!

A global array — bdev_map for block and cdev_map for character devices — is used to implement a hash table, which employs the device major number as hash key. Both cdev_map and bdev_map are instances of the same data structure, struct kobj_map. The hashing method is quite simple: major % 255.

"drivers/base/map.c"

struct kobj_map {
    struct probe {
        struct probe *next;
        //device number, both major and minor are contained
        dev_t dev;
        //The consecutive range of minor numbers
        unsigned long range;	
        struct module *owner;
        kobj_probe_t *get;
        int (*lock)(dev_t, void *); 
        void *data;
    } *probes[255];
    struct mutex *lock;
};

Character Device Range Database

A second database is for character devices only(but why…?).
Again a hash table is employed to keep track of previously allocated device number ranges, and again the major number is used as hash key.

static struct char_device_struct {
    struct char_device_struct *next;
    unsigned int major;
    unsigned int baseminor;
    int minorct;
    char name[64];
    struct cdev *cdev;      /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

Registration Procedures

Register or allocate a range of device numbers first…

<fs.h>

int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

After device number has been obtained, initializing an instance of struct cdev with cdev_init, followed by a call to cdev_add, then, the device is added into character device database.

<cdev.h>

void cdev_init(struct cdev *cdev, const struct file_operations *fops); 
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

Connection with filesystem

Every file in Linux filesystem is managed by inode, the device driver related elements is showed as below.

"linux/fs.h"

struct inode {
	 ...
	dev_t		i_rdev;	//device number
	...
	umode_t		i_mode;	//file type
	...
	struct file_operations *i_fop; 
	...
	union {
		...
		struct block_device * i_bdev;
		struct cdev * i_cdev;
	};
	...
}

When open a device file, VFS will eventually call init_special_inode to create inode for character(block) device.

"fs/inode.c"

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
    inode->i_mode = mode;
    if (S_ISCHR(mode)) {
        inode->i_fop = &def_chr_fops;
        inode->i_rdev = rdev;
    } else if (S_ISBLK(mode)) {
        inode->i_fop = &def_blk_fops;
        inode->i_rdev = rdev;
    } else if (S_ISFIFO(mode))
        inode->i_fop = &pipefifo_fops;
    else if (S_ISSOCK(mode))
        ;   /* leave it no_open_fops */
    else 
        printk("error....");
}
"fs/devices.c"

struct file_operations def_chr_fops = {
	.open = chrdev_open,
};

Character Devices Operation

character devices representation

<cdev.h>

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops; 
	struct list_head list;		//all inodes that represent device special files for the device
	dev_t dev;					//device number
	unsigned int count;
};

character devices open

when user space program open a char device file, the call stack is like below:

sys_open
	->do_sys_open
		->do_filp_open
			->path_open_at
				->vfs_open
					->do_dentry_open
						->chrdev_open
	fd_install
  • inode->i_cdev points to the selected cdev instance. When the inode is opened next time, the character device database need not be queried anymore because the cached value can be used.
  • The inode is added to cdev->list (i_devices is used as the list element in the inode).
  • file->f_ops, that is, the file_operations for struct file, are set to point to the file_operations instance given by struct cdev.

Ioctl(input/output control interface)

Ioctl command

ioctl is used to transfer is used to transfer control command between user space program and device driver module, it’s control message is composed of cmd and arg. cmd is a u32 variable, listed as below:

  • NR: command number
  • TYPE: a ascii character, AKA. magic number
  • SIZE: arg number
  • DIR: data transfer direction, looking from user space side, 4 different conditions, none, read, write, read_write.
    There are macros used to construct cmd:
"include/uapi/asm-generic/ioctl.h"
#define _IOC(dir,type,nr,size) \
    (((dir)  << _IOC_DIRSHIFT) | \
     ((type) << _IOC_TYPESHIFT) | \
     ((nr)   << _IOC_NRSHIFT) | \
     ((size) << _IOC_SIZESHIFT))

/*
 * Used to create numbers.
 *
 * NOTE: _IOW means userland is writing and kernel is reading. _IOR
 * means userland is reading and kernel is writing.
 */
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOR_BAD(type,nr,size)  _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

Ioctl system call

iotcl system call would eventually call unlocked_ioctl defined by device driver.

"fs/ioctl.c"

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
    return ksys_ioctl(fd, cmd, arg);
}

int ksys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
{
    int error;
    struct fd f = fdget(fd);
    
  	...
    if (!error)
        error = do_vfs_ioctl(f.file, fd, cmd, arg);
    fdput(f);
    return error;
}

int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
         unsigned long arg)
{
    int error = 0;
    int __user *argp = (int __user *)arg;
    struct inode *inode = file_inode(filp);

    switch (cmd) {
    case FIOCLEX:
        set_close_on_exec(fd, 1);
        break;

    case FIONCLEX:
        set_close_on_exec(fd, 0);
        break;

	...
	 default:
        if (S_ISREG(inode->i_mode)) // is it a regular file ?
            error = file_ioctl(filp, cmd, arg);
        else
            error = vfs_ioctl(filp, cmd, arg);
        break;
    }
    return error;
}

long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int error = -ENOTTY;

    if (!filp->f_op->unlocked_ioctl)
        goto out;

    error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
    if (error == -ENOIOCTLCMD)
        error = -ENOTTY;
 out:
    return error;
}
EXPORT_SYMBOL(vfs_ioctl);

Demo

sun_char driver

#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include <linux/uaccess.h>

static struct cdev chr_dev;
static dev_t ndev;
static char buf[256];
static int buf_len;

static int chr_open(struct inode *nd, struct file *filp)
{
	int major = MAJOR(nd->i_rdev);
	int minor = MINOR(nd->i_rdev);
	printk("chr_open, major=%d, minor=%d\n", major, minor);
	return 0;
}

static ssize_t chr_read(struct file *file, char __user *buffer, size_t len, loff_t *off)
{
	int ret = 0;
	ret = copy_to_user(buffer, buf, buf_len);

	if(ret == 0) {
		printk(KERN_INFO "Send %d characters to user\n", buf_len);
		buf_len = 0;
		return 0;
	} else {
		printk(KERN_INFO "Failed sending...\n");
		return -EFAULT;
	}
}

static ssize_t chr_write(struct file *file, const char __user *buffer, size_t len, loff_t *off)
{
	sprintf(buf, "%s(%zu letters)", buffer, len);
	buf_len = strlen(buf);
	printk(KERN_INFO "Receive %zu characters from user\n", len);
	return len;
}

struct file_operations chr_ops =
{
	.owner	=	THIS_MODULE,
	.open	=	chr_open,
	.read	=	chr_read,
	.write	= 	chr_write,
};

static int demo_init(void)
{
	int ret;
	cdev_init(&chr_dev, &chr_ops);
	ret = alloc_chrdev_region(&ndev, 0, 1, "sun_char");
	if(ret < 0)
		return ret;
	printk("demo init major=%d, minor=%d\n", MAJOR(ndev), MINOR(ndev));
	ret = cdev_add(&chr_dev, ndev, 1);
	if(ret < 0)
		return ret;
		
	return 0;
}

static void demo_exit(void)
{
	printk("Removing chr_dev demo..\n");
	cdev_del(&chr_dev);
	unregister_chrdev_region(ndev, 1);
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("flynn");
MODULE_DESCRIPTION("A char device driver..");

Test app

Before test, you may have to use mknod /dev/sun_chr c <major> <minor> to create a device file. and use sudo to run this app.

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

int main()
{
	int ret;
	char buf[256];
	char string[256];
	int fd = open("/dev/sun_char", O_RDWR);
	
	if(fd < 0) {
		printf("error %d open sun_char...\n", fd);
		return fd;
	}

	printf("Type in a short string to send to the kernel module:\n");
	scanf("%[^\n]%*c", string);
	printf("Writing message to the device [%s].\n", string);
	ret = write(fd, string, strlen(string));
	if (ret < 0){
		perror("Failed to write the message to the device.");
		return errno;
	}
	
	printf("Press ENTER to read back from the device...\n");
	getchar();
	
	printf("Reading from the device...\n");
	ret = read(fd, buf, 256);
	if (ret < 0){
		perror("Failed to read the message from the device.");
		return errno;
	}
	printf("The received message is: [%s]\n", buf);

	close(fd);
}

References

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值