블로킹과 논블로킹 IO 개요
1. IO의 기본 개념
IO(Input/Output)는 애플리케이션이 디바이스 드라이버와 데이터를 교환하는 작업을 의미한다. 애플리케이션이 디바이스 자원에 접근할 때, 자원을 즉시 확보할 수 없으면 블로킹 IO는 해당 스레드를 일시 중단(suspend)시킨다. 반면 논블로킹 IO는 스레드를 중단시키지 않고,要么轮询等待直到资源可用,要么直接返回错误。
블로킹 IO 처리 흐름
애플리케이션이 read 함수를 호출하여 디바이스에서 데이터를 읽으려 할 때, 디바이스가 사용 불가능하거나 데이터가 준비되지 않았으면 프로세스는休眠 상태로 전환된다. 디바이스가 사용 가능해지면 커널이 프로세스를 깨우고 데이터를 애플리케이션에 반환한다.
논블로킹 IO 처리 흐름
논블로킹 방식으로 디바이스에 접근할 때, 데이터가 준비되지 않으면 즉시 오류 코드를 반환한다. 애플리케이션은 이 오류를 처리하고 계속 재시도하여 데이터를 성공적으로 읽을 때까지 반복한다.
애플리케이션 구현 예시
블로킹 방식으로 디바이스 열기:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR);
ret = read(fd, &data, sizeof(data));
논블로킹 방식으로 디바이스 열기:
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);
ret = read(fd, &data, sizeof(data));
기본적으로 디바이스는 블로킹 IO로 열리며, O_NONBLOCK 플래그를 추가하면 논블로킹 모드로 변경된다.
2. 대기 큐(Wait Queue) 구현
블로킹 IO의 핵심은 디바이스가 사용 불가능할 때 프로세스를 대기시키는 것이다. 리눅스 커널은 대기 큐를 통해 이를 구현한다. 프로세스가休眠 상태에 있으면 CPU 자원을 다른 작업에 양보할 수 있고, 디바이스가 사용 가능해지면 깨어나 작업을 계속한다.
대기 큐 헤드 정의
대기 큐를 사용하려면 먼저 대기 큐 헤드를 생성하고 초기화해야 한다:
struct wait_queue_head_t wq_head;
init_waitqueue_head(&wq_head);
#define DECLARE_WAIT_QUEUE_HEAD(name)
대기 큐 항목 생성
각 프로세스는 개별적인 대기 큐 항목을 가진다:
DECLARE_WAITQUEUE(wait_item, current);
여기서 current는 현재 실행 중인 프로세스를 가리키는 커널 전역 변수이다.
대기 큐 항목 추가/제거
void add_wait_queue(struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry);
void remove_wait_queue(struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry);
프로세스를 대기 상태로 만들려면 해당 프로세스의 대기 항목을 대기 큐 헤드에 추가해야 한다. 디바이스가 사용 가능해지면 대기 항목을 제거하고 프로세스를 깨운다.
프로세스 깨우기
void wake_up(struct wait_queue_head *wq_head);
void wake_up_interruptible(struct wait_queue_head *wq_head);
wake_up은 모든 상태의 프로세스를 깨우지만, wake_up_interruptible은 TASK_INTERRUPTIBLE 상태의 프로세스만 깨운다.
대기 이벤트 함수
| 함수 | 설명 |
| wait_event(wq, condition) | condition이 참이 될 때까지 차단. TASK_UNINTERRUPTIBLE 상태 |
| wait_event_timeout(wq, condition, timeout) | 타임아웃 추가 가능, 반환값으로 조건 충족 여부 확인 |
| wait_event_interruptible(wq, condition) | 시그널에 의해 중단 가능 |
| wait_event_interruptible_timeout(wq, condition, timeout) | 시그널 중단 + 타임아웃 기능 |
3. 사용자 공간 폴링(Polling)
논블로킹 IO에서는 디바이스가 준비되었는지 확인하기 위해 폴링을 사용해야 한다. select, poll, epoll 함수들이 있다.
select 함수
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
fd_set 조작 매크로:
void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
select를 사용한 비블로킹 읽기 예제:
void main(void)
{
int ret, fd;
fd_set readfds;
struct timeval timeout;
fd = open("dev_xxx", O_RDWR | O_NONBLOCK);
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 0;
timeout.tv_usec = 500000;
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (ret) {
case 0:
printf("timeout\n");
break;
case -1:
printf("error\n");
break;
default:
if (FD_ISSET(fd, &readfds)) {
read(fd, &data, sizeof(data));
}
break;
}
}
poll 함수
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events;
short revents;
};
주요 이벤트:
POLLIN 데이터 읽기 가능
POLLOUT 데이터 쓰기 가능
POLLERR 오류 발생
POLLHUP 드바이스 닫힘
poll을 사용한 예제:
void main(void)
{
int ret;
int fd;
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK);
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, 1, 500);
if (ret > 0) {
read(fd, &data, sizeof(data));
}
}
epoll 함수
select와 poll은监听 파일 디스크립터 수가 증가할수록 성능이 저하된다. epoll은 고并发 처리에 최적화되어 있다.
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
주요 이벤트:
EPOLLIN 읽기 가능
EPOLLOUT 쓰기 가능
EPOLLET 엣지 트리거 모드
EPOLLONESHOT 일회성 모니터링
epoll 사용 예제:
void main(void)
{
int ret, fd, epfd;
struct epoll_event event;
epfd = epoll_create(1);
fd = open(filename, O_RDWR | O_NONBLOCK);
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
ret = epoll_wait(epfd, &event, 1, 500);
if (ret > 0) {
read(fd, &data, sizeof(data));
}
}
4. 커널 공간 poll 연산 함수
애플리케이션에서 select 또는 poll을 호출하면 드라이버의 poll 함수가 실행된다:
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
반환 가능한 상태:
POLLIN 읽기 가능
POLLOUT 쓰기 가능
POLLERR 오류
POLLHUP 드바이스 중단
poll_wait 함수는 프로세스를 차단하지 않고 poll_table에 대기 큐를 추가한다:
void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);
블로킹 IO 실습
드라이버 구현
이전 챕터의 IRQ 예제를 기반으로 블로킹 IO를 구현해보자. 주요 변경점은 다음과 같다:
#include <linux/wait.h>
#include <linux/sched.h>
enum key_state {
KEY_IDLE = 0,
KEY_PRESSED,
KEY_RELEASED
};
struct key_device {
dev_t devno;
struct cdev chrdev;
struct class *cls;
struct device *dev;
struct device_node *node;
int gpio_number;
int irq_number;
struct timer_list debounce_timer;
atomic_t key_state;
wait_queue_head_t read_wait;
};
static struct key_device key_dev;
static ssize_t key_read(struct file *filp, char __user *buf,
size_t count, loff_t *offset)
{
int ret;
ret = wait_event_interruptible(key_dev.read_wait,
KEY_IDLE != atomic_read(&key_dev.key_state));
if (ret)
return ret;
ret = copy_to_user(buf, &key_dev.key_state, sizeof(int));
atomic_set(&key_dev.key_state, KEY_IDLE);
return ret;
}
static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &key_dev.read_wait, wait);
if (KEY_IDLE != atomic_read(&key_dev.key_state))
mask = POLLIN | POLLRDNORM;
return mask;
}
static int __init key_driver_init(void)
{
int ret;
init_waitqueue_head(&key_dev.read_wait);
atomic_set(&key_dev.key_state, KEY_IDLE);
ret = alloc_chrdev_region(&key_dev.devno, 0, 1, "key");
if (ret < 0)
return ret;
cdev_init(&key_dev.chrdev, &key_fops);
key_dev.chrdev.owner = THIS_MODULE;
cdev_add(&key_dev.chrdev, key_dev.devno, 1);
key_dev.cls = class_create(THIS_MODULE, "key");
key_dev.dev = device_create(key_dev.cls, NULL, key_dev.devno, NULL, "key");
timer_setup(&key_dev.debounce_timer, key_timer_func, 0);
return 0;
}
테스트 및 결과
드라이버를 컴파일하고 로드한 후 애플리케이션을 실행하면 CPU 사용률이 크게 감소한다. 블로킹 IO를 사용하지 않으면 애플리케이션이 계속轮询하며 CPU를 점유하지만, 블로킹 IO를 사용하면 키 이벤트가 발생할 때만 깨어나 CPU를 효율적으로 사용한다.
논블로킹 IO 실습
드라이버 구현
논블로킹 IO를 구현하려면 O_NONBLOCK 플래그를 확인하고 poll 함수를 구현해야 한다:
static ssize_t key_read(struct file *filp, char __user *buf,
size_t count, loff_t *offset)
{
int ret;
if (filp->f_flags & O_NONBLOCK) {
if (KEY_IDLE == atomic_read(&key_dev.key_state))
return -EAGAIN;
} else {
ret = wait_event_interruptible(key_dev.read_wait,
KEY_IDLE != atomic_read(&key_dev.key_state));
if (ret)
return ret;
}
ret = copy_to_user(buf, &key_dev.key_state, sizeof(int));
atomic_set(&key_dev.key_state, KEY_IDLE);
return ret;
}
애플리케이션 구현
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>
int main(int argc, char *argv[])
{
int fd, ret;
struct pollfd pfd;
int key_value;
if (argc != 2) {
printf("Usage: %s /dev/key\n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDONLY | O_NONBLOCK);
if (fd < 0) {
printf("Failed to open device\n");
return -1;
}
pfd.fd = fd;
pfd.events = POLLIN;
while (1) {
ret = poll(&pfd, 1, -1);
if (ret > 0) {
if (pfd.revents & POLLIN) {
read(fd, &key_value, sizeof(key_value));
if (key_value == 0)
printf("Button Pressed\n");
else
printf("Button Released\n");
}
}
}
close(fd);
return 0;
}
테스트 결과
논블로킹 IO를 사용하면 select나 poll을 통해 디바이스 상태를 확인하면서도 애플리케이션이 대기 상태를 유지할 수 있다. 이를 통해 효율적인 IO 처리가 가능하다.