前言
进程间通信(IPC)底层原理都是需要借助内核来完成的。而且大部分都需要用户空间主动发起,内核仅作为一个中转。
那么内核主动向用户空间发送信息的方式有哪些呢?
Netlink 是一种,在 WiFi 子系统中,WiFi 驱动通过 Netlink 跟用户空间的 hostapd/wpa_supplicant 通信,可以主动向上层传递 WiFi 底层状态信息。
那除了 Netlink,还有别的方式吗?
信号(Signal)也是一种。
信号的基本概念
信号(Signal)是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求是类似的。信号是异步的,进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号的功能介绍
所谓同步,就是“你慢我等你”。
那么异步就是:你慢那你就自己玩,我做自己的事去了,你有情况再通知我。
所谓异步通知,就是 APP 可以忙自己的事,当驱动程序有数据时它会主动给 APP 发信号,这会导致 APP 执行信号处理函数。
仔细想想“发信号”,这只有 3 个字,却可以引发很多问题:
① 谁发:驱动程序发
② 发什么:信号
③ 发什么信号:SIGIO
④ 怎么发:内核里提供有函数
⑤ 发给谁:APP,APP 要把自己告诉驱动
⑥ APP 收到后做什么:执行信号处理函数
⑦ 信号处理函数和信号,之间怎么挂钩:APP 注册信号处理函数
小孩通知妈妈的事情有很多:饿了、渴了、想找人玩。
Linux 系统中也有很多信号,在 Linux 内核源文件 include\uapi\asm-generic\signal.h 中,有很多信号的宏定义:
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29 // 驱动常用信号,表示有 I/O 事件
#define SIGPWR 30
#define SIGSYS 31
信号的处理方式
信号可以被以下几种方式处理:
- 忽略:接收到信号后不做任何反应。
- 捕捉:为信号定义一个处理函数,当信号发生时,执行该处理函数。
- 默认操作:执行系统默认的操作,如终止进程或产生core dump。
SIGIO 的使用
驱动程序通知 APP 时,它会发出“SIGIO”这个信号,表示有“I/O 事件”要处理。
APP 代码流程
就 APP 而言,你想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟 SIGIO 挂钩。这可以通过一个 signal 函数来“给某个信号注册处理函数”,用法如下:
除了注册 SIGIO 的处理函数,APP 还要做什么事?想想这几个问题:
① 内核里有那么多驱动,你想让哪一个驱动给你发 SIGIO 信号?
APP 要打开驱动程序的设备节点。
② 驱动程序怎么知道要发信号给你而不是别人?
APP 要把自己的进程 ID 告诉驱动程序。
③ APP 有时候想收到信号,有时候又不想收到信号:
应该可以把 APP 的意愿告诉驱动:设置 Flag 里面的 FASYNC 位为 1,使能“异步通知”。
驱动代码流程
驱动程序要做什么?发信号。
① APP 设置进程 ID 时,驱动程序要记录下进程 ID ;
② APP 还要使能驱动程序的异步通知功能,驱动中有对应的函数:
③ APP 打开驱动程序时,内核会创建对应的 file 结构体, file 中有 f_flags ; f_flags 中有一个 FASYNC 位,它被设置为 1 时表示使能异步通知功能。 当 f_flags 中的 FASYNC 位发生变化时,驱动程序的 fasync 函数被调用。
④ 发生中断有数据时,驱动调用内核辅助函数发信号。 这个辅助函数名为 kill_fasync。
综上所述,使用异步通知,也就是使用信号的流程如下图所示:
① 打开驱动程序对应的设备节点文件,使用 fd 文件描述符记录
② APP 给 SIGIO 这个信号注册信号处理函数 func ,以后 APP 收到 SIGIO 信号时,这个函数会被自动调用;
③ 把 APP 的 PID( 进程 ID) 告诉驱动,此调用不涉及驱动程序,在内核的文件系统层次记录 PID ;
④ 读取驱动程序文件 Flag ;
⑤ 设置 Flag 里面的 FASYNC 位为 1 :当 FASYNC 位发生变化时,会导致驱动程序的 fasync 被调用;
⑥⑦ 调 用 faync_helper , 它 会 根 据 FAYSNC 的值决定是否设置 button_async->fa_file=驱动文件 filp :驱动文件 filp 结构体里面含有之前设置的 PID 。
⑧ APP 可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用 kill_fasync 发信号;
⑪⑫⑬ APP 收到信号后,它的信号处理函数被自动调用,可以在里面调用 read 函数读取键值。
驱动编程
使用异步通知时,驱动程序的核心有 2 :
① 提供对应的 drv_fasync 函数;
② 并在合适的时机发信号。
drv_fasync 函数很简单,调用 fasync_helper 函数就可以,如下:
static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{
return fasync_helper (fd, filp, on, &button_async);
}
fasync_helper 函数会分配 、构造一个 fasync_struct 结构体 button_async:
- 驱动文件的 flag 被设置为 FAYNC 时:
button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID - 驱动文件被设置为非 FASYNC 时:
button_async->fa_file = NULL;
以后想发送信号时,使用 button_async 作为参数就可以,它里面“可能” 含有 PID 。
什么时候发信号呢?在本例中,在 GPIO 中断服务程序中发信号。
怎么发信号呢? kill_fasync (&button_async, SIGIO, POLL_IN);
① 第 1 个参数: button_async->fa_file 非空时,可以从中得到 PID ,表示发给哪一个 APP ;
② 第 2 个参数表示发什么信号: SIGIO ;
③ 第 3 个参数表示为什么发信号: POLL_IN ,有数据可以读了。 (APP 用不到这个参数)
APP 编程
① 编写信号处理函数:
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
② 注册信号处理函数:
signal(SIGIO, sig_func);
③ 打开驱动:
fd = open(argv[1], O_RDWR);
④ 把进程 ID 告诉驱动(实际上只会到达内核层次,不会到达驱动层次):
fcntl(fd, F_SETOWN, getpid());
⑤ 使能驱动的 FASYNC 功能:
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
SIGIO 实例
源码目录结构
nvidia@nvidia-desktop:~/sigio$ tree
.
├── driver
│ ├── hello.c
│ └── Makefile
├── Makefile
├── test.c
└── write.c
1 directory, 5 files
驱动程序
hello.c
#include <asm/signal.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kdev_t.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/uaccess.h>
static dev_t dev_num;
struct device *dev;
struct class *cls;
struct fasync_struct *hello_fasync;
struct cdev hello_cdev;
struct class hello_class;
static int hello_open(struct inode *inode, struct file *filep)
{
printk("hello_open()\n");
return 0;
}
static int hello_release(struct inode *inode, struct file *filep)
{
printk("hello_release()\n");
return 0;
}
#define KMAX_LEN 32
char kbuf[KMAX_LEN] = "\0";
static ssize_t hello_read(struct file *filep, char __user *buf, size_t size, loff_t *pos)
{
int error;
size = (size <= KMAX_LEN) ? size : KMAX_LEN;
if (copy_to_user(buf, kbuf, size)) {
error = -EFAULT;
return error;
}
return size;
}
static ssize_t hello_write(struct file *filep, const char __user *buf, size_t size, loff_t *pos)
{
int error;
size = (size <= KMAX_LEN) ? size : KMAX_LEN;
memset(kbuf, 0, sizeof(kbuf));
if (copy_from_user(kbuf, buf, size)) {
error = -EFAULT;
return error;
}
printk("%s\n", kbuf);
kill_fasync(&hello_fasync, SIGIO, POLLIN);
return size;
}
int hello_fasync_func(int fd, struct file *filep, int on)
{
printk("hello_fasync \n");
return fasync_helper(fd, filep, on, &hello_fasync);
}
static struct file_operations hello_ops = {
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
.fasync = hello_fasync_func,
};
static int hello_init(void)
{
int result = -1;
printk("hello_init \n");
if (alloc_chrdev_region(&dev_num, 0, 1, "hellodev")) {
printk("%s: can not get device number.", __func__);
result = -EPERM;
goto dev_reg_error;
}
cdev_init(&hello_cdev, &hello_ops);
if ((result = cdev_add(&hello_cdev, dev_num, 1)) != 0) {
goto cdev_add_error;
}
cls = class_create(THIS_MODULE, "hellocls");
if (IS_ERR(cls)) {
printk(KERN_ERR "class_create() failed for cls\n");
result = PTR_ERR(cls);
goto class_create_error;
}
dev = device_create(cls, NULL, dev_num, NULL, "hellodev");
if (IS_ERR(dev)) {
printk(KERN_ERR "device_create failed\n");
result = PTR_ERR(dev);
goto device_create_error;
}
return 0;
device_create_error:
class_destroy(cls);
class_create_error:
cdev_del(&hello_cdev);
cdev_add_error:
unregister_chrdev_region(dev_num, 1);
dev_reg_error:
return result;
}
static void hello_exit(void)
{
printk("hello_exit \n");
device_destroy(cls, dev_num);
class_destroy(cls);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m = hello.o
all:
make -C /lib/modules/$(shell uname -r)/build/ M=${PWD} modules
clean:
make -C /lib/modules/$(shell uname -r)/build/ M=${PWD} clean
APP
test.c
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
char buff[64] = {0};
int fd;
void func(int signo)
{
printf("signo= %d\n", signo);
read(fd, buff, sizeof(buff));
printf("buff=%s\n", buff);
return;
}
int main(int argc, char *argv[])
{
int flage;
fd = open("/dev/hellodev", O_RDWR);
if (fd < 0) {
perror("open fail \n");
return -1;
}
fcntl(fd, F_SETOWN, getpid());
flage = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flage | FASYNC);
signal(SIGIO, func);
while (1)
;
close(fd);
return 0;
}
write.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
int len;
if (argc < 2) {
printf("usage:%s str\n", argv[0]);
return 0;
}
fd = open("/dev/hellodev", O_RDWR);
if (fd < 0) {
perror("open fail \n");
return -1;
}
printf("before write\n");
len = write(fd, argv[1], strlen(argv[1]) + 1);
printf("after write\n");
printf("len = %d\n", len);
close(fd);
return 0;
}
Makefile
all:
gcc test.c -o test
gcc write.c -o write
clean:
rm test write
编译
驱动
nvidia@nvidia-desktop:~/sigio/driver$ make
make -C /lib/modules/5.10.120-tegra/build/ M=/home/nvidia/sigio/driver modules
make[1]: Entering directory '/usr/src/linux-headers-5.10.120-tegra-ubuntu20.04_aarch64/kernel-5.10'
CC [M] /home/nvidia/sigio/driver/hello.o
MODPOST /home/nvidia/sigio/driver/Module.symvers
CC [M] /home/nvidia/sigio/driver/hello.mod.o
LD [M] /home/nvidia/sigio/driver/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.10.120-tegra-ubuntu20.04_aarch64/kernel-5.10'
APP
nvidia@nvidia-desktop:~/sigio$ make
gcc test.c -o test
gcc write.c -o write
运行