【linux查看端口占用】Linux 高级字符驱动操作 iotcl及阻塞IO

更新时间:2019-12-08    来源:php安装    手机版     字体:

【www.bbyears.com--php安装】

Linux设备驱动 高级字符驱动操作之iotcl

大部分驱动除了提供对设备的读写操作外,还需要提供对硬件控制的接口,比如查询一个framebuffer设备能提供多大的分辨率,读取一个RTC设备的时间,设置一个gpio的高低电平等等。而这些对硬件操作能力的实现一般都是通过ioctl方法来实现的

1. 原型介绍

Ioctl在用户空间的原型为:

int ioctl(int fd, unsigned long cmd, ...);

原型中的点不表示一个变数目的参数, 而是一个

单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查. 第 3

个参数的实际特点依赖所发出的特定的控制命令( 第 2 个参数 ). 一些命令不用参数, 一些用一个整

数值, 以及一些使用指向其他数据的指针. 使用一个指针是传递任意数据到 ioctl 调用的方法; 设备接着可与用户空间交换任何数量的数据.

ioctl在内核空间的原型为:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd

参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管

它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的

arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能对此作出警告。

2. iotcl cmd的选择

在实现ioctl之前,我们应该来定义一组ioctl命令,一种简单的方法是使用一组简单的数字来标示,比如从0到9。这种情况一般也没有问题,但是最好不要这样做,ioctl的cmd应该是在系统内是唯一的,这样可以防止向错误的设备发出正确的命令。而如果ioctl命令在系统内是唯一的,那么就不会发生这种情况。

Linux中把ioctl cmd划分成几个位段来帮助创建唯一的cmd。这几个位段一般是:type(模数),序号,传输方向和参数大小。在定义的时候可以参考include/asm/ioctl.h 和 Documentation/ioctl-number.txt两个文件,头文件定义了构建cmd命令的宏,而ioctl-number.txt列举了内核中已经使用的tpye,为了唯一性,尽量不要和这里的type重叠。

以下是这几个位段的简单介绍:

type

魔数. 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中. 这个成员是 8 位宽(_IOC_TYPEBITS).

number

序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.

direction

数据传送的方向,如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.

size

涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值. 你使用这个 size 成员不是强制的 - 内核不检查它 -- 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构, 但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员.

下面是一个定义ioctl命令的展示:

/* Use 'k' as magic number */
#define SCULL_IOC_MAGIC 'k'
/* Please use a different 8-bit number in your code */
 
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
 * S means "Set" through a ptr,
 * T means "Tell" directly with the argument value
 * G means "Get": reply by setting through a pointer
 * Q means "Query": response is on the return value
 * X means "eXchange": switch G and S atomically
 * H means "sHift": switch T and Q atomically
 */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
 
#define SCULL_IOC_MAXNR 14

关于_IOWR等宏定义更多的内容可参考头文件中的定义。

3. IOCTL的返回值

IOCTL的实现往往都是一个switch case语句,返回值依赖每个case分支的实现。当遇到没有定义的cmd时改返回什么值呢,我建议使用-EINVAL,表示无用的参数。另外一点,在case分支比较多的时候,有些人大意常常会忘记写break,导致后面的case分支同样执行,导致错误发生。

4. IOCTL的arg参数

有些ioctl命令并不需要arg这个参数,而大部分ioctl需要在应用层和内核层传递数据,就需要用到这个参数。当arg参数是一个整形的时候,非常简单,我们直接拿来使用就可以了。如果是一个指针,就需要小心一些。

应用层和内核层的数据交换我们常用的是copy_from_user 和 copy_to_user 函数,它们可用来安全地用来移动数据。 这些函数也可用在 ioctl 方法中。但是ioctl中的数据项常常是很小的数据,用这两个函数有点笨重了,我们可以尝试使用其他的方式来实现数据的传递。

int access_ok(int type, const void *addr, unsigned long size);

这个函数用来检查给定的地址是否满足特定的访问需求,这个函数只检查而没有数据copy。使用access_ok之后就可以安全地传输数据。可使用下面的接口来做数据的传输:

put_user(datum, ptr)
       __put_user(datum, ptr)
这些宏定义写 datum 到用户空间;它们相对快,且应当被调用来代替 copy_to_user 无论何时要传送单个值时。这些宏已被编写来允许传递任何类型的指针到 put_user, 只要它是一个用户空间地址。传送的数据大小依赖 prt 参数的类型,并且在编译时使用 sizeof 和 typeof 等编译器内建宏确定。结果是,如果 prt 是一个 char 指针,传送一个字节,以及对于 2, 4, 和 可能的 8 字节。

put_user 检查来确保这个进程能够写入给定的内存地址,它在成功时返回 0,并且在错误时返回 -EFAULT。 __put_user 进行更少的检查(它不调用 access_ok),但是仍然能够失败如果被指向的内存对用户是不可写的。因此, __put_user 应当只用在内存区已经用 access_ok 检查过的时候。

作为一个通用的规则,当你实现一个 read 方法时,调用 __put_user 来节省几个周期,或者当你拷贝几个项时,因此, 在第一次数据传送之前调用 access_ok 一次, 如同上面 ioctl 所示。

get_user(local, ptr)
       __get_user(local, ptr)
       
这些宏定义用来从用户空间接收单个数据。它们象 put_user 和 __put_user,但是在相反方向传递数据。获取的值存储于本地变量 local; 返回值指出这个操作是否成功。再次, __get_user 应当只用在已经使用 access_ok 校验过的地址。




Linux设备驱动 高级字符驱动操作之阻塞IO

比如一个进程调用read读取数据,当没有数据可读时该怎么办,是立即返回还是等到有数据的时候;另一种情况是进程调用write向设备写数据,如果缓冲区满了或者设备正忙的时候怎么办,是立即返回还是继续等待直到设备可写?这种情况下,一般的缺省做法是使进程睡眠直到请求可以满足为止。本篇就介绍遇到这类问题驱动的处理方法。
睡眠

什么是睡眠?一个进程睡眠意味着它暂时放弃了CPU的运行权,直到某个条件发生后才可再次被系统调度。

在驱动里面很容易使一个进程进入睡眠状态,但是这里有几个规则需要特别注意。

    原子上下文不能睡眠。这意味着驱动在持有一个自旋锁, seqlock, 或者 RCU 锁时不能睡眠。
    关闭中断的情况下不能睡眠。在中断处理函数中不能睡眠。
    在持有信号量时可以睡眠,但是会造成其他等待的进程也会进入睡眠,所以应该特别注意,睡眠时间应很短。
    在被唤醒后应做一些必要的检查,确定你等待的条件已经满足。因为你不知道睡眠的这段时间发生了什么。
    睡眠前确定能被唤醒,否则不要睡眠。

如何睡眠和唤醒

睡眠的进程会进入等待队列,一个等待队列可以如下声明:

DECLARE_WAIT_QUEUE_HEAD(name);
或者动态地, 如下:

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

当一个进程需要睡眠,可以调用下面的接口:

//进程被置为不可中断的睡眠,一般不要这样
wait_event(queue, condition)
//它可能被信号中断,此版本应该检查返回值,若返回非零则可能是被某些信号打断,驱动应///该返回-ERESTARTSYS.
wait_event_interruptible(queue, condition)
//下面两个等待一段时间,超时后返回0.
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)


要唤醒休眠的进程,那么其他的进程要调用唤醒函数:

//以下函数唤醒所有的在给定队列上等待的进程,一般情况下带interruptible的配对,不带//的配对
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

阻塞和非阻塞的选择

上面说了睡眠的方法,这种实现就是阻塞IO的实现,还有一种情况是要求不管IO是否可用,调用都要立即返回,就是非阻塞的实现。比如read时,虽然没有数据可读,但是我不想等待,我要立马返回。

非阻塞的IO由 filp->f_flags 中的 O_NONBLOCK 标志来指示,这个标志位于<linux/fcntl.h>, 被 <linux/fs.h>自动包含。这个标志可以在open的时候指定。

缺省状态下IO是阻塞的(没有指定O_NONBLOCK的情况下),在实现read/write的时候需要符合下面的标准:

    如果一个进程调用 read 但是没有数据可用(尚未), 这个进程必须阻塞. 这个进程在有数据达到时被立刻唤醒, 并且那个数据被返回给调用者, 即便小于在给方法的 count 参数中请求的数量。
    如果一个进程调用 write 并且在缓冲中没有空间, 这个进程必须阻塞, 并且它必须在一个与用作 read 的不同的等待队列中. 当一些数据被写入硬件设备, 并且在输出缓冲中的空间变空闲, 这个进程被唤醒并且写调用成功, 尽管数据可能只被部分写入如果在缓冲只没有空间给被请求的 count 字节。

这两句话都假设有输入和输出缓冲,实际上也是这样,几乎每个设备驱动都有输入输出缓冲。缓冲提高了访问效率,防止了数据的丢失。

如果指定O_NONBLOCK,即非阻塞的访问。read和write的做法是不同的。在这种情况下,这些调用简单的返回-EAGAIN。只有read,write和open文件操作收到非阻塞标志的影响。

下面是一个简单的read的实现,其中兼容了阻塞和非阻塞的实现(关键地方以添加注释):

static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
        struct scull_pipe *dev = filp->private_data;
        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;
        while (dev->rp == dev->wp)
        { /* nothing to read */
                up(&dev->sem); /* release the lock */
                //判断是否是阻塞访问,如果是非阻塞访问,那么立即返回-EAGAIN.
                if (filp->f_flags & O_NONBLOCK)
                        return -EAGAIN;
                PDEBUG("\\"%s\\" reading: going to sleep\\n", current->comm); 
                //如果是阻塞访问,那么睡眠等待,等到读条件满足时继续执行。
                if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
                        return -ERESTARTSYS; /* signal: tell the fs layer to handle it */ /* otherwise loop, but first reacquire the lock */
                if (down_interruptible(&dev->sem))
                        return -ERESTARTSYS;
        }
        /* ok, data is there, return something */
        
        //以下即正常读取数据。
        if (dev->wp > dev->rp)
                count = min(count, (size_t)(dev->wp - dev->rp));
        else /* the write pointer has wrapped, return data up to dev->end */
                count = min(count, (size_t)(dev->end - dev->rp));
        if (copy_to_user(buf, dev->rp, count))
        {
                up (&dev->sem);
                return -EFAULT;
        }
        dev->rp += count;
        if (dev->rp == dev->end)
                dev->rp = dev->buffer; /* wrapped */
        up (&dev->sem);
        /* finally, awake any writers and return */
        wake_up_interruptible(&dev->outq);
        PDEBUG("\\"%s\\" did read %li bytes\\n",current->comm, (long)count);
        return count;
}



 
互斥等待

之前我们说过当一个进程调用wake_up后,所有这个队列上等待的进程被置为可运行的。一般情况下这样是没有问题的,但是在个别的情况下,可能提前知道只有一个被唤醒的进程将成功获得需要的资源,并且其他的进程将再次睡眠。如果等待的进程太多,全部唤醒在进入睡眠这样的操作也是耗费资源的,会降低系统的性能。为了应对这种情况,内核中添加了一个互斥等待的选项。这样的结果是,进行互斥等待的进程被一次唤醒一个。

本文来源:http://www.bbyears.com/jiaocheng/82693.html

热门标签

更多>>

本类排行