Openwrt按键检测分析-窥探Linux内核与用户空间通讯机制netlink使用

首先看一下Openwrt系统中关于按键功能的使用和修改,以18.06版本为例

按键功能实现在脚本中, 比如18.06/package/base-files/files/etc/rc.button/reset

#!/bin/sh

. /lib/functions.sh

OVERLAY="$( grep ' /overlay ' /proc/mounts )"

case "$ACTION" in
pressed)
	[ -z "$OVERLAY" ] && return 0

	return 5
;;
timeout)
	. /etc/diag.sh
	set_state failsafe
;;
released)
	if [ "$SEEN" -lt 1 ]
	then
		echo "REBOOT" > /dev/console
		sync
		reboot
	elif [ "$SEEN" -ge 5 -a -n "$OVERLAY" ]
	then
		echo "FACTORY RESET" > /dev/console
		jffs2reset -y && reboot &
	fi
;;
esac

return 0

主要有2个参数, ACTION和SEEN,分别代表按键动作(按下/抬起)和按键持续时间

关于按键GPIO的修改位于dts中,比如target/linux/ramips/dts/GL-MT300N-V2.dts

gpio-keys {
		compatible = "gpio-keys-polled";
		#address-cells = <1>;
		#size-cells = <0>;
		poll-interval = <20>;
		
		vccin {
			label = "BTN_0";
			gpios = <&gpio0 6 0>;
			linux,code = <BTN_0>;
		};
		electricity {
			label = "BTN_1";
			gpios = <&gpio0 11 0>;
			linux,code = <BTN_1>;
		};
		reset {
			label = "reset";
			gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
			linux,code = <KEY_RESTART>;
		};
	};

poll-interval = <20>; 表示轮询检测,防抖时间

label = "reset"; 代表功能实现脚本的名称,对应18.06/package/base-files/files/etc/rc.button/reset

gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; 表示使用GPIO1分组,第7个引脚; 低电平生效

linux,code = <KEY_RESTART>; 按键事件代码,对于linux标准输入输出系统,参考Linux内核头文件input/input.h

compatible = "gpio-keys-polled"; 表示加载驱动gpio-keys-polled

位于18.06/package/kernel/gpio-button-hotplug/src/gpio-button-hotplug.c

它具体实现主要内容如下:

1. 注册2种类型驱动,轮询检测和中断检测

2. 使用netlink与用户空间通讯

3. 自定义上报事件内容, 比如前面功能实现脚本中用到的ACTION以及SEEN等

分析内核驱动,首先要看全局结构体定义,这几乎是分析linux所有驱动的共性

2个主要结构

struct gpio_keys_platform_data *pdata; 满足linux内核驱动模型, platform总线驱动设备描述结构,主要存放的是dts中关于设备的描述信息

struct gpio_keys_button_dev *bdev;  驱动私有描述结构

其二者关系为,

1. 取出pdata信息初始化bdev

2. 把bdev设置为pdata的私有数据,与其他驱动接口同步,platform_set_drvdata(pdev, bdev);

以上的操作为linux驱动的标准执行过程,有兴趣的同学可以深入研究下Linux内核驱动模型

对于驱动来说,最重要的是驱动自己的私有结构,即gpio_keys_button_dev

struct gpio_keys_button_dev {
	int polled;   //是否轮询
	struct delayed_work work;   //用于轮询时的循环检测

	struct device *dev;  //设备,来自pdev
	struct gpio_keys_platform_data *pdata; //platform总线设备描述
	struct gpio_keys_button_data data[0];//事件上报描述结构
};

事件描述gpio_keys_button_data

struct gpio_keys_button_data {
	struct delayed_work work;  //执行上报动作
	struct bh_priv bh;
	int last_state;   //状态记录
	int count;     //按键计时
	int threshold;  //防抖域值
	int can_sleep;   //是否支持睡眠
	struct gpio_keys_button *b;  //中断设备描述
};

介绍完驱动结构描述,下面看其使用

首先看轮询检测:

static struct platform_driver gpio_keys_polled_driver = {
	.probe	= gpio_keys_polled_probe,
	.remove	= gpio_keys_remove,
	.driver	= {
		.name	= "gpio-keys-polled",
		.owner	= THIS_MODULE,
		.of_match_table = of_match_ptr(gpio_keys_polled_of_match),
	},
};

主要实现函数gpio_keys_polled_probe

static int gpio_keys_polled_probe(struct platform_device *pdev)
{
	struct gpio_keys_platform_data *pdata;
	struct gpio_keys_button_dev *bdev;
	int ret;
	int i;

//从platform总线设备描述gpio_keys_platform_data获取信息,
//初始化驱动私有结构gpio_keys_button_dev
	ret = gpio_keys_button_probe(pdev, &bdev, 1);

	if (ret)
		return ret;

//初始化工作队列
	INIT_DELAYED_WORK(&bdev->work, gpio_keys_polled_poll);

	pdata = bdev->pdata;

	if (pdata->enable)
		pdata->enable(bdev->dev);

	for (i = 0; i < pdata->nbuttons; i++)
//逐个检测button状态,准备上报事件数据gpio_keys_button_data
		gpio_keys_polled_check_state(&bdev->data[i]);

//循环检测button状态
	gpio_keys_polled_queue_work(bdev);

	return ret;
}

具体事件上报位于button_hotplug_create_event

static int button_hotplug_create_event(const char *name, unsigned int type,
		unsigned long seen, int pressed)
{
	struct bh_event *event;

	BH_DBG("create event, name=%s, seen=%lu, pressed=%d\n",
		name, seen, pressed);

	event = kzalloc(sizeof(*event), GFP_KERNEL);
	if (!event)
		return -ENOMEM;

// 填充事件信息
	event->name = name;
	event->type = type;
	event->seen = seen;
	event->action = pressed ? "pressed" : "released";

// 在工作队列中上报事件
	INIT_WORK(&event->work, (void *)(void *)button_hotplug_work);
	schedule_work(&event->work);

	return 0;
}

上报事件的netlink实现button_hotplug_work

static void button_hotplug_work(struct work_struct *work)
{
	struct bh_event *event = container_of(work, struct bh_event, work);
	int ret = 0;

//分配skb
	event->skb = alloc_skb(BH_SKB_SIZE, GFP_KERNEL);
	if (!event->skb)
		goto out_free_event;

//填充内容
	ret = bh_event_add_var(event, 0, "%s@", event->action);
	if (ret)
		goto out_free_skb;

	ret = button_hotplug_fill_event(event);
	if (ret)
		goto out_free_skb;

//发送netlink消息
	NETLINK_CB(event->skb).dst_group = 1;
	broadcast_uevent(event->skb, 0, 1, GFP_KERNEL);

 out_free_skb:
	if (ret) {
		BH_ERR("work error %d\n", ret);
		kfree_skb(event->skb);
	}
 out_free_event:
	kfree(event);
}

整个流程总结: 周期循环调用gpio_keys_polled_probe检测按键状态,满足条件发送netlink消息

gpio_keys_polled_probe

        --> gpio_keys_polled_check_state

                --> button_hotplug_create_event

                        -->button_hotplug_work

                                --> broadcast_uevent

有了以上的分析基础,中断检测流程就变得简单了

static struct platform_driver gpio_keys_driver = {
	.probe	= gpio_keys_probe,
	.remove	= gpio_keys_remove,
	.driver	= {
		.name	= "gpio-keys",
		.owner	= THIS_MODULE,
		.of_match_table = of_match_ptr(gpio_keys_of_match),
	},
};

主要流程如下:

gpio_keys_probe

  --> devm_request_threaded_irq  注册中断

    --> button_handle_irq 中断处理函数

        --> button_hotplug_create_event

            -->button_hotplug_work

                -->broadcast_uevent

关于Netlink

Netlink套接字是用以实现用户进程内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。

在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如

  • 路由 daemon(NETLINK_ROUTE)
  • 用户态 socket 协议(NETLINK_USERSOCK)
  • 防火墙(NETLINK_FIREWALL)
  • netfilter 子系统(NETLINK_NETFILTER)
  • 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
  • 通用netlink(NETLINK_GENERIC)

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。

Openwrt系统中netlink消息接收,位于procd-2018-03-28-dfb68f85/plug/hotplug.c

procd是openwrt系统的init进程,负责内核netlink消息接收处理,watchdog以及执行一些循环任务等等

void hotplug(char *rules)
{
	struct sockaddr_nl nls = {};
	int nlbufsize = 512 * 1024;

	rule_file = strdup(rules);
	nls.nl_family = AF_NETLINK;
	nls.nl_pid = getpid();
	nls.nl_groups = -1;

//创建netlink socket
	if ((hotplug_fd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT)) == -1) {
		ERROR("Failed to open hotplug socket: %m\n");
		exit(1);
	}
	if (bind(hotplug_fd.fd, (void *)&nls, sizeof(struct sockaddr_nl))) {
		ERROR("Failed to bind hotplug socket: %m\n");
		exit(1);
	}

	if (setsockopt(hotplug_fd.fd, SOL_SOCKET, SO_RCVBUFFORCE, &nlbufsize, sizeof(nlbufsize)))
		ERROR("Failed to resize receive buffer: %m\n");

	json_script_init(&jctx);
	queue_proc.cb = queue_proc_cb;
//循环接收netlink消息
	uloop_fd_add(&hotplug_fd, ULOOP_READ);
}

调用功能实现脚本时机, procd收到netlink消息后会根据配置文件来搜寻相关功能实现脚本

 按键事件的配置文件为 procd/etc/hotplug.json

[ "if",
    		[ "and",
			[ "has", "BUTTON" ],
			[ "eq", "SUBSYSTEM", "button" ]
		],
		[ "button", "/etc/rc.button/%BUTTON%" ]
	],

  可解读为: procd收到来自BUTTON的消息,最终执行/etc/rc.button/下的脚本,脚本名称为事件中的button字段取值, 在驱动中以数组方式定义,如下:

static struct bh_map button_map[] = {
	BH_MAP(BTN_0,			"BTN_0"),
	BH_MAP(BTN_1,			"BTN_1"),
	BH_MAP(BTN_2,			"BTN_2"),
	BH_MAP(BTN_3,			"BTN_3"),
	BH_MAP(BTN_4,			"BTN_4"),
	BH_MAP(BTN_5,			"BTN_5"),
	BH_MAP(BTN_6,			"BTN_6"),
	BH_MAP(BTN_7,			"BTN_7"),
	BH_MAP(BTN_8,			"BTN_8"),
	BH_MAP(BTN_9,			"BTN_9"),
	BH_MAP(KEY_BRIGHTNESS_ZERO,	"brightness_zero"),
	BH_MAP(KEY_CONFIG,		"config"),
	BH_MAP(KEY_COPY,		"copy"),
	BH_MAP(KEY_EJECTCD,		"eject"),
	BH_MAP(KEY_HELP,		"help"),
	BH_MAP(KEY_LIGHTS_TOGGLE,	"lights_toggle"),
	BH_MAP(KEY_PHONE,		"phone"),
	BH_MAP(KEY_POWER,		"power"),
	BH_MAP(KEY_RESTART,		"reset"),
	BH_MAP(KEY_RFKILL,		"rfkill"),
	BH_MAP(KEY_VIDEO,		"video"),
	BH_MAP(KEY_WIMAX,		"wwan"),
	BH_MAP(KEY_WLAN,		"wlan"),
	BH_MAP(KEY_WPS_BUTTON,		"wps"),
};

BH_MAP第一个参数BTN_0或KEY_POWER对应input子系统的input.h中标准定义;

第二个参数"BTN_0"或"reset"对应/etc/rc.button/下的脚本名称

至此,openwrt按键检测整个过程分析完毕

最后总结:

1. Openwrt系统按键内核驱动分为两种, 循环检测和中断检测, 在dts中配置

2. 按键驱动通过netlink方式发送按键事件到用户空间

3. 在用户空间, init进程procd统一接收处理来自内核的netlink消息,同时根据配置文件,调用/etc/rc.button/下的脚本, 此处需注意脚本的可执行权限

4. dts中关于按键的配置, linux,code为input子系统标准事件定义,即input.h中的定义

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值