字符设备驱动(5)

-- TOC --

接上一篇,继续完善mychar驱动,这次增加了如下新功能:

  1. 支持并发读写,使用semaphore;
  2. 提供一个驱动参数,在insmod的时候使用;
  3. 支持llseek接口,mychar也支持任意位置的读写;
  4. 不再直接调用printk接口,而是使用pr_*接口。

代码如下:

$ cat mychar.c
#define pr_fmt(fmt) "%s:%s:%d: " fmt, KBUILD_MODNAME, __func__, __LINE__

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");

#define MINOR_FIRST   3
#define MINOR_NUM     4

static unsigned int major = 0;
static unsigned int minor_pos = 0; /* count of successful cdev_add */

static unsigned long content_len = 1024*1024;  // 1MiB by default
module_param(content_len, ulong, S_IRUGO);

struct file_operations mychar_fop;
struct mychar_dev {
    char *cont;
    size_t len;
    struct semaphore sema;
    struct cdev mycdev;
} *pmychar = NULL;


int mychar_open(struct inode *inode, struct file *fp) {
    unsigned int flags;

    /* check flags */
    flags = fp->f_flags & O_ACCMODE;
    if ((flags != O_RDONLY) && (flags != O_WRONLY))
        return -EPERM;  // operation not permitted

    fp->private_data = (void *)container_of(inode->i_cdev,
                                            struct mychar_dev, mycdev);
    return 0;
}


int mychar_release(struct inode *inode, struct file *fp) {
    return 0;
}


ssize_t mychar_read(struct file *fp, char __user *buf,
                    size_t count, loff_t *f_pos) {
    unsigned long left;
    struct mychar_dev *dev=(struct mychar_dev *)fp->private_data;

    if (down_interruptible(&dev->sema))
        return -ERESTARTSYS;

    pr_info("read called, count %zu.\n", count);

    if (*f_pos >= dev->len) {
        pr_info("read call end, return count 0.\n");
        up(&dev->sema);
        return 0;
    }

    /* update count if reach end */
    if ((*f_pos+count) > dev->len)
        count = dev->len - *f_pos;

    /* must use copy_to_user, can't derefer user-space */
    if ((left = copy_to_user(buf,dev->cont+*f_pos,count))) {
        if (left != count) {
            count -= left;
            pr_err("copy_to_user return less than count!\n");
        }
        else {
            up(&dev->sema);
            return -EFAULT;
        }
    }

    /* update *f_pos */
    *f_pos += count;

    /* return how many char readed */
    pr_info("read call end, return count %zu.\n", count);
    up(&dev->sema);
    return count;
}


ssize_t mychar_write(struct file *fp, const char __user *buf,
                     size_t count, loff_t *f_pos) {
    unsigned long left;
    struct mychar_dev *dev=(struct mychar_dev *)fp->private_data;

    if (down_interruptible(&dev->sema))
        return -ERESTARTSYS;

    pr_info("write called, count %zu, *f_pos=%lld.\n", count, *f_pos);

    if (*f_pos >= content_len) {
        pr_info("write call end, return count 0.\n");
        up(&dev->sema);
        return -ENOSPC;
    }

    if ((*f_pos+count) > content_len)
        count = content_len - *f_pos;

    if ((left = copy_from_user(dev->cont+*f_pos,buf,count))) {
        if (left != count) {
            count -= left;
            pr_err("copy_from_user return less than count!\n");
        }
        else {
            up(&dev->sema);
            return -EFAULT;
        }
    }

    *f_pos += count;
    if (dev->len < *f_pos)
        dev->len = *f_pos;  // update real length
    pr_info("write call end, return count %zu.\n", count);
    up(&dev->sema);
    return count;
}


loff_t mychar_llseek(struct file *fp, loff_t offs, int whence) {
    struct mychar_dev *dev=(struct mychar_dev *)fp->private_data;
    loff_t pos;

    pr_info("seek called, offset = %lld, whence = %d\n", offs, whence);

    switch (whence) {
        case 0:  // SEEK_SET
            pos = offs;
            break;
        case 1:  // SEEK_CUR
            pos = fp->f_pos + offs;
            break;
        case 2:  // SEEK_END
            pos = dev->len + offs;
            break;
        default:
            return -EINVAL;
    }

    if (pos < 0) return -EINVAL;
    /* modify fp->f_pos directly here */
    fp->f_pos = pos;
    pr_info("new position: %lld\n", pos);
    return pos;
}


struct file_operations mychar_fop = {
    .owner   = THIS_MODULE,
    .open    = mychar_open,
    .release = mychar_release,
    .read    = mychar_read,
    .write   = mychar_write,
    .llseek  = mychar_llseek,
};


static void mychar_exit(void) {
    int i;

    if (pmychar != NULL) {
        for (i=0; i<MINOR_NUM; ++i)
            if (pmychar[i].cont != NULL) kfree(pmychar[i].cont);
        for (i=0; i<minor_pos; ++i)
            cdev_del(&pmychar[i].mycdev);
        kfree(pmychar);
    }
    unregister_chrdev_region(MKDEV(major,MINOR_FIRST), MINOR_NUM);
    pr_notice("exit.\n");
}


static int __init mychar_init(void) {
    int rn, i;
    dev_t dev;

    /* get a major number */
    if ((rn = alloc_chrdev_region(&dev,MINOR_FIRST,MINOR_NUM,"mychar"))) {
        pr_warn("can't get major number, err %d.\n", rn);
        return rn;
    }
    major = MAJOR(dev);
    pr_info("major is %d, %d minor start from %d.\n",
                major, MINOR_NUM, MINOR_FIRST);

    /* alloc mychar, init mutex */
    pmychar = kmalloc(MINOR_NUM*sizeof(struct mychar_dev), GFP_KERNEL);
    if (pmychar == NULL) {
        mychar_exit();
        return -ENOMEM;
    }
    memset(pmychar, 0, MINOR_NUM*sizeof(struct mychar_dev));
    for (i=0; i<MINOR_NUM; ++i) {
        if ((pmychar[i].cont=kmalloc(content_len,GFP_KERNEL)) == NULL) {
            mychar_exit();
            return -ENOMEM;
        }
        sema_init(&pmychar[i].sema, 1); // 1 is for mutex
    }
    pr_info("alloc %lu bytes memory for all successfully.\n", content_len);

    /* init and add cdev */
    for (i=0; i<MINOR_NUM; ++i) {
        cdev_init(&pmychar[i].mycdev, &mychar_fop);
        if((rn = cdev_add(&pmychar[i].mycdev,MKDEV(major,MINOR_FIRST+i),1))){
            pr_err("cdev_add err %d, minor %d.\n", 
                        rn, i+MINOR_FIRST);
            mychar_exit();
            return rn;
        }
        ++minor_pos;
    }
    pr_notice("%d devices added successfully.\n", minor_pos);

    /* success return */
    return 0;
}


module_init(mychar_init);
module_exit(mychar_exit);

定义semaphore,struct semaphore sema;,这个定义在mychar_dev结构体中,每个device一个,因为device之间没有共享数据,这样实现更自然和高效。

初始化semaphore,sema_init接口,初始化值为1,此时就是mutex!一定要在cdev_add之前完成初始化。

semaphore的P操作(-1操作)有好几个接口可以使用:

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);

down decrements the value of the semaphore and waits as long as need be. down_interruptible does the same, but the operation is interruptible. The interruptible version is almost always the one you will want; it allows a user-space process that is waiting on a semaphore to be interrupted by the user. You do not, as a general rule, want to use noninterruptible operations unless there truly is no alternative. Non-interruptible operations are a good way to create unkillable processes (the dreaded “D state” seen in ps), and annoy your users. Using down_interruptible requires some extra care, however, if the operation is interrupted, the function returns a nonzero value, and the caller does not hold the semaphore. Proper use of down_interruptible requires always checking the return value and responding accordingly. The final version down_trylock never sleeps; if the semaphore is not available at the time of the call, down_trylock returns immediately with a nonzero return value.

用ps命令有时看到进程的状态是D,一般都表示此进程在死等IO,估计是使用了down接口。

mychar使用down_interruptible接口,需要判断返回值,如果不是0,所有在等待时,被用户终止了调用进程。

Note the check on the return value of down_interruptible; if it returns nonzero, the operation was interrupted. The usual thing to do in this situation is to return -ERESTARTSYS. Upon seeing this return code, the higher layers of the kernel will either restart the call from the beginning or return the error to the user. If you return -ERESTARTSYS , you must first undo any user-visible changes that might have been made, so that the right thing happens when the system call is retried. If you cannot undo things in this manner, you should return -EINTR instead.

返回码也很讲究,优先选-ERESTARTSYS,如果想偷懒,直接返回-EINTR。

semaphore的V操作(+1操作)很简单,就是up接口。

通过semaphore,将读写接口的critical section包起来,就支持了并发。

要想支持在加载驱动的时候,传递一个参数进去,需要使用module_param这组接口,它们定义在<linux/moduleparam.h>文件中,C语言基本类型都支持,请仔细阅读注释。在加载驱动的时候传递参数,insmod mychar content_len=1024,在load.sh中修改这一行即可。

S_IRUGO表示所有人可读,这一组macro定义在<linux/stat.h>文件中,如下:

#include <asm/stat.h>
#include <uapi/linux/stat.h>

#define S_IRWXUGO       (S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO       (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO         (S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO         (S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO         (S_IXUSR|S_IXGRP|S_IXOTH)

那么,在哪里可读呢?这里:

$ pwd
/sys/module/mychar/parameters
$ cat content_len
1024
$ ll content_len
-r--r--r-- 1 root root 4096 12月 16 10:19 content_len

llseek接口的实现比较简单,需要注意一个细节,在这个接口的实现中,我们是直接修改fp->f_pos的值!这与read和write接口简介修改参数*f_pos不一样!

使用pr_*系列接口,简化了代码,在文件最前面自定义pr_fmt(fmt)这个macro,可以实现log输出的统一风格。

最后是测试,我写了一个测试脚本:

$ cat test.py
import multiprocessing


s1 = '012345678901234\n'
s2 = 'abcdefghijklmno\n'
s3 = 'cs.pynote.net..\n'


def write_string(string):
    with open('mychar-3','wb') as f:
        a = 0
        while True:
            f.write(string.encode())
            f.flush()
            a += 1
            if (a == 63):
                a = 0
                f.seek(0)


def read_string():
    while True:
        with open('mychar-3') as f:
            lines = f.readlines()
            for line in lines:
                if not line in (s1, s2, s3):
                    print('read err')


th1 = multiprocessing.Process(target=write_string,
                       args=('012345678901234\n',), daemon=True)
th2 = multiprocessing.Process(target=write_string,
                       args=('abcdefghijklmno\n',), daemon=True)
th3 = multiprocessing.Process(target=write_string,
                       args=('cs.pynote.net..\n',), daemon=True)

th4 = multiprocessing.Process(target=read_string, args=(), daemon=True)

th1.start()
th2.start()
th3.start()
th4.start()
th1.join()
th2.join()
th3.join()
th4.join()

3个进程不停地去写mychar-3,一个进程不停地去读mychar-3,如果看不见打印err,就说明semaphore有效果了!当然,测试成功。

本文链接:https://cs.pynote.net/sf/linux/dd/202112161/

-- EOF --

-- MORE --