/* AUTHOR: Pinus
* Creat on : 2018-10-11
* KERNEL : linux-4.4.145
* BOARD : JZ2440(arm9 s3c2440)
* REFS : 韋東山視頻教程第二期
*/
概述
已有的方式:
1. 查詢:極度耗費資源
2. 中斷:可以沉睡但app會一直運行
3. poll:可以指定沉睡時間
共同點:都是應用程序主動去調用驅動
異步通知機制全稱是"信號驅動的異步IO",就是一旦設備就緒,則驅動主動通知應用程序,應用程序根本就不需要輪詢設備,類似於硬件的中斷概念,一個進程收到一個信號與處理器收到一箇中斷請求可以說是一樣的。
在linux中,異步通知是使用signal(信號)這裏使用的是信號"SIGIO"來實現的,而在linux,大概有30種信號,比如大家熟悉的ctrl+c的SIGINT信號,和常用進程間發信號 kill -9 PID 結束某一個進程。
1.應用層程序將自己註冊爲接收來自設備文件的SIGIO信號的進程
2.驅動實現相應的接口,以期具有向所有註冊接收這個設備驅動SIGIO信號的應用程序發SIGIO信號的能力。
3.驅動在適當的位置調用發送函數,應用程序即可接收到SIGIO信號,運行信號處理函數
實驗
目標: 按鍵按下時驅動程序通知應用程序
1. 應用程序:註冊信號,信號處理函數
在Linux命令行中使用 man signal 查找signal的用法
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
可見signal()函數指定信號(signum)的處理函數(handler),當程序接收到指定信號時調用處理函數,處理函數(handler)由自己編寫(有些信號系統有默認處理函數)。
man fcntl
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ ); //根據不同的cmd,和不同的參數進行不同的操作
fcntl(fd, F_SETOWN, getpid()); // 告訴內核,發給誰
/*
F_SETOWN
Set the process ID or process group ID that will receive SIGIO
and SIGURG signals for events on file descriptor fd to the ID
given in arg. A process ID is specified as a positive value; a
process group ID is specified as a negative value. Most com‐
monly, the calling process specifies itself as the owner (that
is, arg is specified as getpid(2)). */
Oflags = fcntl(fd, F_GETFL); //獲取文件標記
/*
F_GETFL (void)
Get the file access mode and the file status flags; arg is
ignored. */
fcntl(fd, F_SETFL, Oflags | FASYNC); // 改變fasync標記,最終會調用到驅動的faync > fasync_helper:每當FASYNC標誌改變時,驅動程序中的fasync()得到執行。驅動中應該事先fasync()函數。
/* F_SETFL (int)
Set the file status flags to the value specified by arg. 還會去調用drv_fasync初始化異步通知鏈表 */
直接上應用程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
/*
* fasync
*/
int fd;
void my_signal_func()
{
unsigned char key_val;
read(fd, &key_val, 1);
printf("key_val: 0x%x\n",key_val);
}
int main(int argc, char **argv)
{
int Oflags;
signal(SIGIO, my_signal_func);
fd = open("/dev/buttons", O_RDWR);
if (fd < 0){
printf("can't open dev nod !\n");
return -1;
}
fcntl(fd, F_SETOWN, getpid());
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC);
while(1){
sleep(1000);
}
return 0;
}
2. 誰發: 驅動; 發給誰: app; 怎麼發: IRQ_handler中:
驅動大致仍然遵循前文案,這裏只列出更改部分的代碼
static struct fasync_struct *button_async;
static const struct file_operations jz2440_buttons_fops = {
.owner = THIS_MODULE,
.read = buttons_drv_read,
.open = buttons_drv_open,
.release = buttons_drv_close,
.poll = buttons_drv_poll,
.fasync = buttons_drv_fasync, /* 新增信號函數 */
};
int buttons_drv_fasync (int fd, struct file *file, int on)
{
printk("driver: button fasync\n");
return fasync_helper(fd, file, on, &button_async); /* 真正有用的 */
}
在按鍵中斷處理程序中發送信號
static irqreturn_t button_irq_handle(int irq, void *dev_id)
{
struct intpin_desc * pindesc = (struct intpin_desc *)dev_id;
unsigned int pinval;
pinval = gpio_get_value(pindesc->pin);
if (pinval){
/*鬆開*/
key_val = 0x80 | pindesc->key_val;
}else{
/*按下*/
key_val = pindesc->key_val;
}
//ev_press = 1;
//wake_up_interruptible(&button_waitq); /*喚醒*/
kill_fasync(&button_async, SIGIO, POLL_IN); /* 信號發送程序 */
return IRQ_HANDLED;
}
信號發送出去後,app便會執行信號處理函數,即讀取按鍵值
分析
1.signal函數,。。。布吉島,功能是綁定信號處理函數
2.fcntl
由以前的經驗也能猜出來了,
/fs/fcntl.c
SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
|
do_fcntl(fd, cmd, arg, f.file);
這個函數好直觀,這裏只看用到的幾個命令
static long do_fcntl(int fd, unsigned int cmd, unsigned long arg, struct file *filp)
{
...
switch (cmd) {
...
case F_GETFL:
err = filp->f_flags;
break;
case F_SETFL:
err = setfl(fd, filp, arg);
break;
..
case F_SETOWN:
f_setown(filp, arg, 1);
err = 0;
break;
...
default:
break;
}
return err;
}
第一個命令比較直觀,跳過
先來分析第三個f_setown()
f_setown(filp, arg, 1)
__f_setown(filp, pid, type, force);
f_modown(filp, pid, type, force);
static void f_modown(struct file *filp, struct pid *pid, enum pid_type type, int force)
{
...
if (force || !filp->f_owner.pid) {
put_pid(filp->f_owner.pid);
filp->f_owner.pid = get_pid(pid); // 設置了程序進程pid
filp->f_owner.pid_type = type;
if (pid) {
const struct cred *cred = current_cred();
filp->f_owner.uid = cred->uid;
filp->f_owner.euid = cred->euid;
}
}
...
}
再來第二個setfl(),這個調用了我們定義的驅動fasync函數
static int setfl(int fd, struct file * filp, unsigned long arg)
{
...
/* 如果設置了FASYNC標誌
* ->fasync() is responsible for setting the FASYNC bit.
*/
if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
//前面接觸了這麼多內核驅動接口,看到filp->f_op->fasync,應該知道這是調用我們註冊的自定義fasync函數了,實際上是調用fasync_helper()
// 感覺這纔是這個函數最大的意義,名字取的不好。。。
...
}
...
//修改flags
filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK);
...
}
我們的底層驅動中幹了什麼?
buttons_drv_fasync
fasync_helper(fd, file, on, &button_async); // static struct fasync_struct *button_async;
/*
* fasync_helper() is used by almost all character device drivers
* to set up the fasync queue, and for regular files by the file
* lease code. It returns negative on error, 0 if it did no changes
* and positive if it added/deleted the entry.
被絕大多數字符設備調用設置fasync(異步通知)隊列,錯誤返回負數,成功添加|刪除入口返回0
*/
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}
/*
* Add a fasync entry. Return negative on error, positive if
* added, and zero if did nothing but change an existing one.
添加一個異步通知入口,錯誤返回負數,添加返回正數,改變存在項返回0
*/
static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
...
/*
* fasync_insert_entry() returns the old (update) entry if
* it existed.
*
* So free the (unused) new entry and return 0 to let the
* caller know that we didn't add any new fasync entries.
如果存在要添加的入口項,則釋放新分配的入口,返回0,讓用戶知道
*/
if (fasync_insert_entry(fd, filp, fapp, new)) {
fasync_free(new);
return 0;
}
return 1;
}
綜上,當應用程序使用F_SETFL命令後,會調用驅動的drv_fasync()->fasync_helper(),經過一系列操作,將當前設備添加進內核異步通知鏈表。
以上所有準備工作都已經完成。
功能實現,是靠:
kill_fasync(&button_async, SIGIO, POLL_IN);
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
...
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
...
}
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
while (fa) {
struct fown_struct *fown;
unsigned long flags;
...
if (fa->fa_file) {
/*
struct fown_struct {
rwlock_t lock; // protects pid, uid, euid fields
int pid; // pid or -pgrp where SIGIO should be sent
uid_t uid, euid; // uid/euid of process setting the owner
void *security;
int signum; // posix.1b rt signal to be delivered on IO
};
*/
fown = &fa->fa_file->f_owner; // fasync_struct ->fa_file->f_owner
//通過異步對象的文件指針知道其屬主進程
...
send_sigio(fown, fa->fa_fd, band);
}
...
}
}
分析到此爲止send_sigio()發送信號的具體細節就不深究了
總結
將進程“添加”到異步通知隊列中,實則是通過文件指針關聯對象,將對象添加到隊列中。定位進程則是反過來,根據文件指針查找,然後根據對應文件指針去找關聯的進程,所以有時候會出現一個文件指針可以找到多個進程(多個進程打開了該文件)。這種情況下,應用程序仍然必須藉助於poll/select來確定輸入的來源。
從用戶程序的角度考慮:爲了啓動文件的異步通知機制,用戶程序必須執行兩個步驟。首先,她們指定一個進程作爲文件的“屬主(owner)”。當進程使用fcntl系統調用執行F_SETOWN命令時,屬主進程的進程ID號就被保存在filp->f_owner中。這一步是必需的,目的是爲了讓內核知道應該通知哪個進程;然後,爲了真正啓動異步通知機制,用戶程序還必須在設備中設置FASYNC標誌,這通過fcntl的F_SETFL命令完成的。 執行完這兩個步驟之後,輸入文件就可以在新數據到達時請求發送一個SIGIO信號。該信號被髮送到存放在filp->f_owner中的進程(如果是負值就是進程組)。
從驅動程序角度考慮:F_SETOWN被調用時對filp->f_owner賦值,此外什麼也不做;在執行F_SETFL啓用FASYNC時,調用驅動程序的fasync方法。只要filp->f_flags中的FASYNC標識發生了變化,就會調用該方法,以便把這個變化通知驅動程序,使其能正確響應。文件打開時,FASYNC標誌被默認爲是清除的。當數據到達時,所有註冊爲異步通知的進程都會被髮送一個SIGIO信號。