- 비동기 알림 개요 ======
1.1 비동기 알림 소개
블로킹 및 논블로킹 방식 모두 애플리케이션이 장치 상태를 주로 확인해야 합니다.
비동기 알림은 드라이버가 접근 가능하다고 보고할 수 있으며, 애플리케이션이 신호를 수신한 후 드라이버 장치에서 데이터를 읽거나 쓰는 방식과 유사합니다.
비동기 알림의 핵심은 신호입니다:
#define SIGHUP 1 /* 터미널 중단 또는 제어 프로세스 종료 */
#define SIGINT 2 /* 터미널 중단(Ctrl+C 조합) */
#define SIGQUIT 3 /* 터미널 종료(Ctrl+\ 조합) */
#define SIGILL 4 /* 불법 명령 */
#define SIGTRAP 5 /* 디버깅 사용, 중단점 명령 생성 */
#define SIGABRT 6 /* abort(3)에서 발생한 종료 명령 */
#define SIGIOT 6 /* IOT 명령 */
#define SIGBUS 7 /* 버스 오류 */
#define SIGFPE 8 /* 부동소수점 연산 오류 */
#define SIGKILL 9 /* 프로세스 강제 종료 */
#define SIGUSR1 10 /* 사용자 정의 신호 1 */
#define SIGSEGV 11 /* 세그먼트 위반(잘못된 메모리 세그먼트) */
#define SIGUSR2 12 /* 사용자 정의 신호 2 */
#define SIGPIPE 13 /* 읽기 전용 파이프에 데이터 쓰기 */
#define SIGALRM 14 /* 알람 */
#define SIGTERM 15 /* 소프트웨어 종료 */
#define SIGSTKFLT 16 /* 스택 예외 */
#define SIGCHLD 17 /* 자식 프로세스 종료 */
#define SIGCONT 18 /* 프로세스 계속 */
#define SIGSTOP 19 /* 프로세스 실행 중단(일시정지) */
#define SIGTSTP 20 /* 프로세스 실행 중지(Ctrl+Z 조합) */
#define SIGTTIN 21 /* 백그라운드 프로세스가 터미널에서 데이터 읽기 필요 */
#define SIGTTOU 22 /* 백그라운드 프로세스가 터미널에 데이터 쓰기 필요 */
#define SIGURG 23 "긴급" 데이터 있음
#define SIGXCPU 24 /* CPU 자원 제한 초과 */
#define SIGXFSZ 25 /* 파일 크기 초과 */
#define SIGVTALRM 26 /* 가상 시계 신호 */
#define SIGPROF 27 /* 시계 신호 설명 */
#define SIGWINCH 28 /* 창 크기 변경 */
#define SIGIO 29 /* 입력/출력 작업 가능 */
#define SIGPOLL SIGIO
#define SIGPWR 30 전원 재시작
#define SIGSYS 31 /* 불법 시스템 호출 */
#define SIGUNUSED 31 /* 사용되지 않는 신호 */
9번과 19번 신호는 무시할 수 없습니다. 이러한 신호는 인터럽트 번호와 유사하며, 다른 인터럽트 번호는 다른 인터럽트를 나타내고, 다른 인터럽트는 다른 기능을 구현합니다.
인터럽트를 사용할 때 인터럽트 처리 함수를 설정해야 하므로, 애플리케이션에서 신호를 사용할 때도 신호 처리 함수가 필요합니다. 애플리케이션에서 signal 함수를 사용하여 신호 처리 함수를 설정합니다:
/*
* @description : 지정된 신호의 처리 함수 설정
* @param - signum : 처리 함수를 설정할 신호
* @param - handler : 신호 처리 함수
* @return : 성공 시 이전 처리 함수 반환, 실패 시 SIG_ERR 반환
*/
sighandler_t signal(int signum, sighandler_t handler);
여기서 신호 처리 프로토타입은 다음과 같습니다:
typedef void (*sighandler_t)(int)
이전에 사용한 kill -9 PID는 프로세스에 SIGKILL 신호를 전송하는 것입니다. CTRL + C를 누르면 현재 터미널을 사용 중인 애플리케이션에 SIGINT(2) 신호가 전송되어 터미널이 중단됩니다.
linux/atk-mpl 폴더에 signaltest.c를 생성하고 다음 코드를 입력합니다:
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
/* 신호 처리 함수 */
void signalint_handler(int num)
{
printf("\r\nSIGINT 신호!\r\n");
exit(0);
}
int main(void)
{
signal(SIGINT, signalint_handler);
while(1);
return 0;
}
/*
전체 흐름: 먼저 자체 신호 처리 함수를 설정한 후 애플리케이션을 시작합니다.
CTRL+C를 눌러 signaltest에 SIGINT 신호를 보내면 signalint_handler 함수가 실행됩니다.
이 함수는 SIGINT signal!을 출력한 후 exit 함수가 signaltest 애플리케이션을 종료합니다.
*/
다음 명령으로 signaltest.c를 컴파일합니다:
gcc signaltest.c -o signaltest
다음 명령으로 signaltest 애플리케이션을 실행합니다:
./signaltest
CTRL+ C를 누르면 다음과 같은 결과가 나타납니다:
1.2 드라이버의 신호 처리
① fasync_struct 구조체
드라이버에서 신호를 사용하려면 먼저 fasync_struct 구조체 변수를 정의해야 합니다. 일반적으로 이 구조체를 xxx_dev 구조체에 배치합니다:
/* 버튼 장치 구조체 */
struct btn_dev{
dev_t devid; /* 장치 번호 */
struct cdev cdev; /* cdev */
struct class *class; /* 클래스 */
struct device *device; /* 장치 */
struct device_node *nd; /* 장치 노드 */
int btn_gpio; /* 버튼에 사용되는 GPIO 번호 */
struct timer_list timer; /* 버튼 값 */
int irq_num; /* 인터럽트 번호 */
atomic_t status; /* 버튼 상태 */
wait_queue_head_t r_wait; /* 읽기 대기 큐 헤드 */
struct fasync_struct *async_queue; /* fasync_struct 구조체 */
};
② fasync 함수
이 함수는 사용자 공간 애플리케이션의 프로세스 ID를 드라이버의 해당 파일 비동기 알림 큐에 추가합니다.
비동기 알림을 사용하려면 드라이버에서 file_operation 작업 집합의 fasync 함수를 구현해야 합니다:
int (*fasync) (int fd, struct file *filp, int on)
드라이버의 fasync 함수 예시는 다음과 같습니다:
struct xxx_dev {
......
struct fasync_struct *async_queue; /* 비동기 관련 구조체 */
};
static int xxx_fasync(int fd, struct file *filp, int on) // fasync 함수
{
struct xxx_dev *dev = (xxx_dev)filp->private_data; // 프라이빗 데이터
if (fasync_helper(fd, filp, on, &dev->async_queue) < 0) // 여기서 fasync_helper는 실제로 fasync에 의해 호출됨
return -EIO;
return 0;
}
static struct file_operations xxx_ops = {
......
.fasync = xxx_fasync, // file_operations에 있는 모든 함수는 이 형식으로 구현해야 합니다
......
};
드라이브 파일을 닫아야 하는 경우 release 함수 내에서 fasync_struct를 해제해야 합니다. 예시는 다음과 같습니다:
static int xxx_release(struct inode *inode, struct file *filp)
{
return xxx_fasync(-1, filp, 0); /* 비동기 알림 삭제 */
}
static struct file_operations xxx_ops = {
......
.release = xxx_release, // file_operations를 사용하는 경우 항상 이렇게 구현합니다
};
③ kill_fasync 함수
/*
* @description : 장치가 접근 가능할 때 드라이버가 애플리케이션에 신호를 보내야 합니다. 이 함수는 지정된 신호를 전송합니다
* @param - fp : 조작할 fasync_struct
* @param - sig : 전송할 신호
* @param - band : 읽기 가능할 때는 POLL_IN, 쓰기 가능할 때는 POLL_OUT으로 설정
* @return : 없음
*/
void kill_fasync(struct fasync_struct **fp, int sig, int band);
1.3 애플리케이션의 비동기 알림 처리
총 3단계로 구성됩니다:
① 신호 처리 함수 등록
여기서의 처리 함수는 직접 설정해야 하며, 마지막으로 signal 함수에 전달해야 합니다.
② 커널에 현재 애플리케이션의 프로세스 번호 알리기
fcntl(fd, F_SETOWN, getpid())를 사용하여 현재 애플리케이션의 프로세스 번호를 커널에 알립니다.
③ 비동기 알림 활성화
flags = fcntl(fd, F_GETFL); /* 현재 프로세스 상태 가져오기 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 현재 프로세스 비동기 알림 기능 활성화 */
핵심은 fcntl 함수를 사용하여 프로세스 상태를 FASYNC로 설정하는 것입니다. 이 단계를 거치면 드라이버의 fasync 함수가 실행됩니다.
사용자 공간 애플리케이션이 fcntl 시스템 호출을 사용하고 F_SETFL 명령과 FASYNC 플래그를 설정하면 커널은 드라이버의 fasync 함수를 호출합니다.
- 프로그램 작성 ======
/linux/atk-mpl/Drivers 폴더에 16_notification 폴더를 생성하고, notification.c 파일을 생성하여 다음 코드를 입력합니다:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/fcntl.h>
#define BUTTON_CNT 1 /* 장치 번호 개수 */
#define BUTTON_NAME "btn" /* 이름 */
/* 버튼 상태 정의 */
enum button_status {
BTN_PRESS = 0, // 버튼 눌림
BTN_RELEASE, // 버튼 떨어짐
BTN_KEEP, // 버튼 상태 유지
};
/* 버튼 장치 구조체 */
struct btn_dev{
dev_t devid; /* 장치 번호 */
struct cdev cdev; /* cdev */
struct class *class; /* 클래스 */
struct device *device; /* 장치 */
struct device_node *nd; /* 장치 노드 */
int btn_gpio; /* 버튼에 사용되는 GPIO 번호 */
struct timer_list timer; /* 버튼 값 */
int irq_num; /* 인터럽트 번호 */
atomic_t status; /* 버튼 상태 */
wait_queue_head_t r_wait; /* 읽기 대기 큐 헤드 */
struct fasync_struct *async_queue; /* fasync_struct 구조체 */
};
static struct btn_dev button; /* 버튼 장치 */
static irqreturn_t btn_interrupt(int irq, void *dev_id) // 인터럽트 처리 함수
{
/* 버튼 디바운싱 처리, 15ms 지연으로 타이머 활성화 */
mod_timer(&button.timer, jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
/*
* @description : 버튼 IO 초기화, open 함수로 드라이브를 열 때
* 버튼에 사용되는 GPIO 핀을 초기화합니다.
* @param : 없음
* @return : 없음
*/
static int btn_parse_dt(void)
{
int ret;
const char *str;
/* LED에 사용되는 GPIO 설정 */
/* 1. 장치 노드 가져오기: btn */
button.nd = of_find_node_by_path("/btn");
if(button.nd == NULL) {
printk("btn 노드를 찾을 수 없습니다!\r\n");
return -EINVAL;
}
/* 2. status 속성 읽기 */
ret = of_property_read_string(button.nd, "status", &str);
if(ret < 0)
return -EINVAL;
if (strcmp(str, "okay"))
return -EINVAL;
/* 3. compatible 속성 값 읽기 및 비교 */
ret = of_property_read_string(button.nd, "compatible", &str);
if(ret < 0) {
printk("btn: Failed to get compatible property\n");
return -EINVAL;
}
if (strcmp(str, "alientek,btn")) {
printk("btn: Compatible match failed\n");
return -EINVAL;
}
/* 4. 장치 트리의 gpio 속성 가져오기, BTN0에 사용되는 KEY 번호 가져오기 */
button.btn_gpio = of_get_named_gpio(button.nd, "btn-gpio", 0);
if(button.btn_gpio < 0) {
printk("btn-gpio를 가져올 수 없습니다");
return -EINVAL;
}
/* 5. GPIO에 해당하는 인터럽트 번호 가져오기 */
button.irq_num = irq_of_parse_and_map(button.nd, 0);
if(!button.irq_num){
return -EINVAL;
}
printk("btn-gpio 번호 = %d\r\n", button.btn_gpio);
return 0;
}
static int btn_gpio_init(void) // gpio 초기화
{
int ret;
unsigned long irq_flags;
ret = gpio_request(button.btn_gpio, "BTN0"); // gpio 요청
if (ret) {
printk(KERN_ERR "btn: Failed to request btn-gpio\n");
return ret;
}
/* GPIO를 입력 모드로 설정 */
gpio_direction_input(button.btn_gpio);
/* 장치 트리에 지정된 인터럽트 트리거 유형 가져오기 */
irq_flags = irq_get_trigger_type(button.irq_num);
if (IRQF_TRIGGER_NONE == irq_flags)
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
/* 인터럽트 요청 */
ret = request_irq(button.irq_num, btn_interrupt, irq_flags, "Btn0_IRQ", NULL);
if (ret) {
gpio_free(button.btn_gpio);
return ret;
}
return 0;
}
static void btn_timer_function(struct timer_list *arg) // 타이머 처리 함수
{
static int last_val = 1;
int current_val;
/* 버튼 값 읽기 및 현재 상태 판단 */
current_val = gpio_get_value(button.btn_gpio);
if (0 == current_val && last_val){
atomic_set(&button.status, BTN_PRESS); // 눌림
wake_up_interruptible(&button.r_wait); // r_wait 큐 헤드의 모든 큐 깨우기
if(button.async_queue)
kill_fasync(&button.async_queue, SIGIO, POLL_IN); //
}
else if (1 == current_val && !last_val) {
atomic_set(&button.status, BTN_RELEASE); // 떨어짐
wake_up_interruptible(&button.r_wait); // r_wait 큐 헤드의 모든 큐 깨우기
if(button.async_queue)
kill_fasync(&button.async_queue, SIGIO, POLL_IN);
}
else
atomic_set(&button.status, BTN_KEEP); // 상태 유지
last_val = current_val;
}
/*
* @description : 장치 열기
* @param - inode : 드라이브에 전달된 inode
* @param - filp : 장치 파일, file 구조체에는 private_data라는 멤버 변수가 있음
* 일반적으로 open 시 private_data를 장치 구조체를 가리키도록 설정합니다.
* @return : 0 성공; 기타 실패
*/
static int btn_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 장치에서 데이터 읽기
* @param – filp : 열릴 장치 파일(파일 서술자)
* @param – buf : 사용자 공간으로 반환될 데이터 버퍼
* @param – cnt : 읽을 데이터 길이
* @param – offt : 파일 시작 주소에 대한 오프셋
* @return : 읽은 바이트 수, 음수일 경우 읽기 실패
*/
static ssize_t btn_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
if (filp->f_flags & O_NONBLOCK) { // 논블로킹 방식 접근
if(BTN_KEEP == atomic_read(&button.status))
return -EAGAIN;
} else { // 블로킹 방식 접근
/* 대기 큐에 추가, 버튼이 눌리거나 떨어질 때만 깨어남 */
ret = wait_event_interruptible(button.r_wait, BTN_KEEP != atomic_read(&button.status));
if(ret)
return ret;
}
/* 버튼 상태 정보를 애플리케이션으로 전송 */
ret = copy_to_user(buf, &button.status, sizeof(int));
/* 상태 재설정 */
atomic_set(&button.status, BTN_KEEP);
return ret;
}
/*
* @description : fasync 함수, 비동기 알림 처리
* @param – fd : 파일 서술자
* @param – filp : 열릴 장치 파일(파일 서술자)
* @param – on : 모드
* @return : 음수는 함수 실행 실패를 의미
*/
static int btn_fasync(int fd, struct file *filp, int on)
{
return fasync_helper(fd, filp, on, &button.async_queue);
}
/*
* @description : 장치에 데이터 쓰기
* @param - filp : 장치 파일, 열린 파일 서술자를 나타냅니다
* @param - buf : 장치에 쓸 데이터
* @param - cnt : 쓸 데이터 길이
* @param - offt : 파일 시작 주소에 대한 오프셋
* @return : 쓴 바이트 수, 음수일 경우 쓰기 실패
*/
static ssize_t btn_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 장치 닫기/해제
* @param - filp : 닫을 장치 파일(파일 서술자)
* @return : 0 성공; 기타 실패
*/
static int btn_release(struct inode *inode, struct file *filp)
{
return btn_fasync(-1, filp, 0);
}
/*
* @description : poll 함수, 논블로킹 접근 처리
* @param - filp : 열릴 장치 파일(파일 서술자)
* @param - wait : 대기 목록(poll_table)
* @return : 장치 또는 리소스 상태,
*/
static unsigned int btn_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &button.r_wait, wait);
if(BTN_KEEP != atomic_read(&button.status)) // 버튼이 눌리거나 떨어짐
mask = POLLIN | POLLRDNORM; // PLLIN 반환
return mask;
}
/* 장치 작업 함수 */
static struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.read = btn_read,
.write = btn_write,
.release = btn_release,
.poll = btn_poll,
.fasync = btn_fasync,
};
/*
* @description : 드라이브 진입 함수
* @param : 없음
* @return : 없음
*/
static int __init mybtn_init(void)
{
int ret;
/* 대기 큐 헤드 초기화 */
init_waitqueue_head(&button.r_wait);
/* 버튼 상태 초기화 */
atomic_set(&button.status, BTN_KEEP);
/* 장치 트리 파싱 */
ret = btn_parse_dt();
if(ret)
return ret;
/* GPIO 인터럽트 초기화 */
ret = btn_gpio_init();
if(ret)
return ret;
/* 문자 장치 드라이브 등록 */
/* 1. 장치 번호 생성 */
ret = alloc_chrdev_region(&button.devid, 0, BUTTON_CNT, BUTTON_NAME); /* 장치 번호 요청 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", BUTTON_NAME, ret);
goto free_gpio;
}
/* 2. cdev 초기화 */
button.cdev.owner = THIS_MODULE;
cdev_init(&button.cdev, &btn_fops);
/* 3. cdev 추가 */
ret = cdev_add(&button.cdev, button.devid, BUTTON_CNT);
if(ret < 0)
goto del_unregister;
/* 4. 클래스 생성 */
button.class = class_create(THIS_MODULE, BUTTON_NAME);
if (IS_ERR(button.class)) {
goto del_cdev;
}
/* 5. 장치 생성 */
button.device = device_create(button.class, NULL, button.devid, NULL, BUTTON_NAME);
if (IS_ERR(button.device)) {
goto destroy_class;
}
/* 6. 타이머 초기화, 타이머 처리 함수 설정, 주기 설정되지 않아 타이머 활성화 안됨 */
timer_setup(&button.timer, btn_timer_function, 0);
return 0;
destroy_class:
device_destroy(button.class, button.devid);
del_cdev:
cdev_del(&button.cdev);
del_unregister:
unregister_chrdev_region(button.devid, BUTTON_CNT);
free_gpio:
free_irq(button.irq_num, NULL);
gpio_free(button.btn_gpio);
return -EIO;
}
/*
* @description : 드라이브 종료 함수
* @param : 없음
* @return : 없음
*/
static void __exit mybtn_exit(void)
{
/* 문자 장치 드라이브 등록 해제 */
cdev_del(&button.cdev);/* cdev 삭제 */
unregister_chrdev_region(button.devid, BUTTON_CNT); /* 장치 번호 등록 해제 */
del_timer_sync(&button.timer); /* 타이머 삭제 */
device_destroy(button.class, button.devid);/*장치 등록 해제 */
class_destroy(button.class); /* 클래스 등록 해제 */
free_irq(button.irq_num, NULL); /* 인터럽트 해제 */
gpio_free(button.btn_gpio); /* IO 해제 */
}
module_init(mybtn_init);
module_exit(mybtn_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
notificationApp.c 파일을 작성합니다:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
static int btn_fd;
/*
* SIGIO 신호 처리 함수
* @param – signum : 신호 값
* @return : 없음
*/
static void sigio_signal_func(int signum)
{
unsigned int btn_val = 0;
read(btn_fd, &btn_val, sizeof(unsigned int));
if (0 == btn_val)
printf("버튼 눌림\n");
else if (1 == btn_val)
printf("버튼 떨어짐\n");
printf("SIGIO 신호!\r\n");
}
/*
* @description : main 메인 프로그램
* @param – argc : argv 배열 요소 개수
* @param – argv : 구체적 매개변수
* @return : 0 성공; 기타 실패
*/
int main(int argc, char *argv[])
{
int flags = 0;
/* 전달된 매개변수 개수가 올바른지 확인 */
if(2 != argc) {
printf("사용법:\n"
"\t./notificationApp /dev/btn\n"
);
return -1;
}
/* 장치 열기 */
btn_fd = open(argv[1], O_RDONLY | O_NONBLOCK); // 논블로킹 O_NONBLOCK
if(0 > btn_fd) {
printf("오류: %s 파일 열기 실패!\n", argv[1]);
return -1;
}
/* SIGIO 신호 처리 함수 설정 */
signal(SIGIO, sigio_signal_func); // signal 함수, 신호 처리 함수 설정
fcntl(btn_fd, F_SETOWN, getpid()); // 현재 프로세스의 프로세스 번호를 커널에 알림
flags = fcntl(btn_fd, F_GETFD); // 현재 프로세스 상태 가져오기
fcntl(btn_fd, F_SETFL, flags | FASYNC); // 프로세스 비동기 알림 기능 활성화
/* 루프로 버튼 데이터 읽기 */
while(1) {
sleep(2);
}
/* 장치 닫기 */
close(btn_fd);
return 0;
}
- 실행 테스트 ======
먼저 Makefile 파일을 작성합니다:
KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := notification.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
그다음 notification.c와 notificationApp 파일을 컴파일합니다:
make
arm-none-linux-gnueabihf-gcc notificationApp.c -o notification
컴파일된 notificationApp과 notification.ko를 다음 위치로 복사합니다:
sudo cp notification notification.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f
개발판을 켜고 다음 명령을 입력합니다:
cd lib/modules/5.4.31/
depmod
modprobe notification.ko
애플리케이션 테스트:
./notification /dev/btn
Btn0를 누르면 다음과 같은 결과가 나타납니다:
Btn0를 누를 때마다 SIGIO 신호를 캡처하고 버튼 값이 성공적으로 가져옵니다.
드라이브 언로드: rmmod notification.ko