最近工作业务方向由native framework层转换到kernel层,主要是维护编解码设备驱动。
对driver这块的研究已经是五六年前的事了,后来就一直在中间件这块展开工作,并且五六年前还是手捧着LDD3业余学习这块,因此,目前亟需补一补这方面的基础知识。例如misc设备驱动怎么用了,多进程并发访问一个设备时,如何管理访问顺序了。
主要涉及以下知识点:
-
misc设备驱动
-
信号量、原子操作、等待队列用法
-
应用层测试方法
本着分享目的,已将测试代码上传至kernel-journey。
需要补充说明一点,driver测试基于kernel-4.14,app测试基于Android4.4平台。
misc设备驱动
提取骨骼框架如下:
static int test_open(struct inode *inode, struct file *filp)
{
... // trigger when open("/dev/api_test", O_RDWR)
}
static int test_release(struct inode *inode, struct file *filp)
{
...// close(fd)
}
static long test_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...// ioctl(fd)
}
static const struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_release,
.unlocked_ioctl = test_ioctl,
.compat_ioctl = NULL,
};
static struct miscdevice test_dev = {
.minor = TEST_MINOR,
.name = "api_test",
.fops = &test_fops,
};
static const struct of_device_id of_match_test_table[] = {
{ .compatible = "xcom,mytest", .data = NULL },
{ },
};
static int test_probe(struct platform_device *pdev)
{
... // trigger when insmod
}
static int test_remove(struct platform_device *pdev)
{
... // trigger when rmmod
}
static struct platform_driver test_driver = {
.probe = test_probe,
.remove = test_remove,
.driver = {
.owner = THIS_MODULE,
.name = "api_test",
.of_match_table = of_match_test_table,
},
};
module_platform_driver(test_driver);
MODULE_DESCRIPTION("KERNEL Common API Test");
MODULE_LICENSE("GPL");
原子、信号量、等待队列
-
atomic_t
定义:这儿
典型使用场景:多进程中共享资源的计数加减。例如,硬件设备往往只有一个,usr space层可能同时存在多个进程在访问该设备,则用此来保存client数。
常用API:
atomic_inc(&g_instance_cnt);
atomic_dec(&g_instance_cnt);
usr_val = atomic_read(&g_instance_cnt);
ret = atomic_inc_return(&g_instance_cnt); //自增,并返回+1后的值
-
struct semaphore
定义:这儿
典型使用场景:用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。即资源有明确访问数量限制的场景,常用于限流。
可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,
显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。
常用API:
sema_init(&g_test_dev.mutex, 1); //资源数初始化为1
up(&g_test_dev.mutex); //使用完了,释放,使资源数+1
down(&g_test_dev.mutex); //block方式获取资源,如果拿不到,则app会被一直block,即使ctrl+c也不能中断等待。不推荐使用,替换用down_interruptible或down_killable
down_timeout(&g_test_dev.mutex, msecs_to_jiffies(usr_val)); //设定超时时间来获得资源
-
wait_queue_t
定义:这儿
典型使用场景:通过睡眠的等待,就是当设备不可用时,由底层驱动来检测,检查,识别设备可用不可用,如果不可用,底层设备驱动就让应用程序进入休眠状态(结果让当前进程的CPU资
源撤下来给别的任务去使用),并且底层驱动能够检查设备可用不可用,如果一旦检查到设备数据可用,再次唤醒休眠的进程(休眠的进程一旦被唤醒,就会获取CPU的资源),
然后去读取数据即可。 例如,对于视频编码器来说,编码一帧需要几十毫秒,当启动了编码器进行编码时,可将该进程加入到等待队列中,直到被唤醒(超时或者编码完成后发出的中断)。
常用API:
init_waitqueue_head(&client->wait_queue);
ret = wait_event_interruptible_timeout(client->wait_queue, 0, msecs_to_jiffies(DEV_TIMEOUT_MS));
其他说明:本来想用定时器中断来模拟硬件的这个行为,无奈定时器中断还不熟悉,没跑起来,后面有空了再补充完整吧。
应用层测试方法
驱动完成了,如何测试驱动是否OK呢?参考下面这块代码(完整版在这儿):
int main(void)
{
ALOGW("-------------ApiTester begin--------------");
const char *dev = "/dev/api_test";
int fd = open(dev, O_RDWR);
ALOGD("open(%s) with ret_fd = %d, strerror=(%s)", dev, fd, strerror(errno));
if (fd < 0) {
ALOGE("open failed! forget to insmod api_test.ko?");
return -1;
}
int val = 0;
int ret = 0;
ALOGD("-----------------IOCTL_GET/SET_VAL-----------------------");
ALOGD("after IOCTL_GET_VAL, val = %d", val);
ioctl(fd, TEST_GET_VAL, &val); //先拿驱动中的值
ALOGD("after IOCTL_GET_VAL, val = %d", val);
ALOGD("wait for key in val and SET to kernel...");
scanf("%d", &val); //再等待usr输入
ioctl(fd, TEST_SET_VAL, &val); //再设置给driver
val = 0;
ioctl(fd, TEST_GET_VAL, &val); //最后再从driver中拿
ALOGD("then GET from kernel, val=%d", val);
ALOGD("-----------------IOCTL_SEMA_DOWN/UP-----------------------");
while (1) {
ALOGD("looping. key in up(1), down(2) or quit(others)...");
scanf("%d", &val);
// 1-up semaphore
if (val == 1) {
ioctl(fd, TEST_SEMA_UP, NULL);
// 2-down semaphore. 又细分为几种down的方式。
} else if (val == 2) {
ALOGD("key in down_type(1-down; 2-interruptible; 3-killable; 4-trylock; others-TimeoutMs)...");
ret = scanf("%d", &val);
if ((ret!=1) || (val<0)) {
ALOGW("error input? for ret=%d, val=%d.", ret, val);
break;
}
ALOGD("begin to down_type(%d)!", val);
ret = ioctl(fd, TEST_SEMA_DOWN, &val);
ALOGD("finish down! DOWN_ret=%d", ret);
ret = 0;
} else {
ALOGW("usr want to quit semaphore test! val=%d", val);
break;
}
}
ALOGD("-----------------IOCTL_INSTANCE_CNT-----------------------");
ret = ioctl(fd, TEST_INSTANCE_CNT, &val); //测试atomic
ALOGD("current dev instance cnt=%d, ret=%d", val, ret);
ALOGD("-----------------IOCTL_WAIT_EVENT-----------------------");
ret = ioctl(fd, TEST_WAIT_EVENT, &val); //测试wait_queue,内部用了timeout 2s的方式,等定时器中断会用了后,再增加这用方式的
ALOGD("dev's task complete=%d, ret=%d", val, ret);
close(fd); //会调用到release()
ALOGW("-------------ApiTester end--------------");
return 0;
}
其他补充
-
编译问题
分为两部分编译,app+driver。在应用层编译没什么好说的,但编译成ko文件需要特别说明一下。
没看到谷歌提供的编译kernel的命令,可能需要各芯片原厂定制吧。但万变不离其宗,所谓编译,无非用交叉编译器将源码编译成ko文件,需要写好Makefile、Kconfig,再调用(指定arch、cross_compiler)命令。
本来刚开始尝试在kernel目录下编译,但没成功,参考了别人在vendor目录下放的一个,就这样就可以编译出ko文件了。 -
insmod后无probe()内输出log
刚开始没在dts文件中指定设备节点,导致没输出,即只进行了module_init(),而没执行probe()。貌似自从有了dts这个东西后,所有设备必须在该文件中进行声明,才能在/dev/目录下看到该设备。 -
kill进程后无release()调用
OS管理了进程所打开的所有fd,当退出时,会清理掉全部fd,即使用户没有主动调用close(),系统会帮你做这个事情。出现如上描述现象,原始猜测是:有些log会被过滤掉,即使log级别为最高等级,但一定会调用到release()。我曾尝试在release()函数内故意访问非法地址,结果系统每次都挂掉。 -
open(dev)/close(dev)
可能有多个app进行了open()/close()操作。driver需要处理好并发访问问题。 -
有时候内核log不输出或延迟输出
猜测是内核设置的机制,冗余(完全重复的行)log可能被剔除来减轻cpu负载。