Stripe가 개발자 경험에 성공한 비밀과 Kinde가 바꾸는 미래
Stripe의 혁신적인 개발자 경험과 Kinde가 이끄는 인증 시스템의 미래를 살펴보고, 개발자 친화적 솔루션 도입 인사이트를 제공합니다.
Shelled AI (한국)
© 2025 Shelled Nuts Blog. All rights reserved.
Capture your moments quietly and securely
Stripe의 혁신적인 개발자 경험과 Kinde가 이끄는 인증 시스템의 미래를 살펴보고, 개발자 친화적 솔루션 도입 인사이트를 제공합니다.
Shelled AI (한국)
복잡한 환경에서 에이전트 협업 시뮬레이션 실습을 통해 멀티 에이전트 시스템의 실제 적용과 사례를 단계별로 체험해보세요.
Shelled AI (한국)
한 번의 API 호출로 인증과 결제를 동시에 처리하는 비밀 패턴을 소개합니다. 개발 효율과 보안을 동시에 향상시키는 최신 웹 개발 팁!
Shelled AI (한국)
어, 또 만났네요! 지난번 "Linux 커널 소스코드 빌드 및 모듈 작성 실습" 글, 어떠셨나요? 댓글에서 많은 분들이 문자(character)와 블록(block) 디바이스 드라이버에 대해 더 알고 싶다고 하셔서, 오늘은 이 주제를 제대로 파헤쳐볼까 해요.
운영체제의 핵심 중 하나가 바로 디바이스 드라이버죠. 특히 문자와 블록 디바이스 드라이버는 하드웨어와 소프트웨어 사이를 이어주는 다리 역할을 합니다. 이 둘을 직접 만들어보면 리눅스 커널의 구조와 동작 원리를 훨씬 깊이 이해할 수 있어요. 저도 처음엔 "이걸 어디서부터 손대야 하지?" 싶어서 한참 헤맸는데, 작은 실수와 삽질을 반복하다 보니 조금씩 감이 오더라고요. 완벽하지 않아도 괜찮아요. 같이 천천히 배워봅시다.
오늘은 문자와 블록 디바이스 드라이버의 역할 차이, 커널 인터페이스와 주요 함수, 실습 코드까지 하나씩 살펴볼 거예요. 읽고 나면 직접 드라이버 뼈대를 만들어볼 수 있는 자신감과 실전 팁을 얻어가실 수 있을 겁니다. 자, 진짜 리눅스 커널 개발자의 길, 한 걸음 더 들어가볼까요?
여러분, 드라이버 개발 처음 접했을 때 어땠나요? 저는 솔직히 "이게 대체 뭐야?" 싶었어요. 커널, 드라이버, 디바이스… 용어만 들어도 머리가 지끈했죠. 그런데 하나씩 뜯어보니 "아, 이런 원리구나!" 하고 감이 오더라고요. 자, 이제 문자와 블록 디바이스 드라이버의 기본 개념과 역할을 차근차근 풀어볼게요.
먼저 문자 디바이스 드라이버부터 볼까요? 이건 데이터를 한 바이트씩, 순차적으로 읽고 쓰는 장치에 쓰입니다. 키보드, 마우스, 시리얼 포트 같은 장치들이 대표적이죠. 저도 라즈베리파이에 UART 통신 붙여봤을 때 문자 디바이스 드라이버를 직접 만져봤는데, 데이터가 한 줄 한 줄 들어오는 게 신기하더라고요. 이 드라이버는 데이터를 바로 처리하기 때문에, 별도의 버퍼링 없이 실시간성이 중요한 환경에 딱 맞아요. 그런데 막상 코딩하다 보면 "왜 한 번에 많이 못 읽지?" 이런 궁금증도 들죠. 그게 바로 문자 디바이스의 특징이에요.
이제 블록 디바이스 드라이버로 넘어가볼게요. 이건 데이터를 일정 크기의 블록 단위로 처리합니다. 하드디스크, SSD, USB 메모리 같은 저장장치에서 주로 쓰이죠. 예를 들어, 여러분이 윈도우에서 C드라이브에 파일을 저장하거나, 리눅스에서 ext4 파일시스템을 사용하는 것도 다 블록 디바이스 덕분이에요. 블록 디바이스의 큰 특징은 랜덤 액세스가 가능하다는 것! 파일의 중간 부분만 쏙 빼올 수도 있고, 캐싱이나 버퍼링을 통해 성능도 쭉쭉 올릴 수 있죠. 실제로 제가 SSD 드라이버 실습하다가 캐싱 옵션 꺼봤더니, 체감 성능이 확 떨어져서 깜짝 놀랐던 기억이 있어요. "아, 이런 게 진짜 중요하구나" 싶었죠.
여기서 중요한 건, 디바이스 드라이버들이 단독으로 존재하는 게 아니라 커널과 긴밀하게 연결된다는 점이에요. 커널은 드라이버와 시스템 콜, 그리고 VFS(가상 파일 시스템)를 통해 소통하죠. 쉽게 말해, 어플리케이션이 "파일 열어줘!" 하면 커널이 그 요청을 드라이버에게 던지고, 드라이버가 실제 하드웨어와 통신해서 데이터를 주고받는 구조입니다. 문자 디바이스는 file_operations
로, 블록 디바이스는 block_device_operations
와 request_queue
로 구현된다는 점도 기억해두세요.
저도 한 번 실수한 적이 있는데요, file_operations 대신 block_device_operations를 등록했다가 장치가 안 떠서 한참 삽질했어요. 이런 기본 구조만 알아도 개발할 때 훨씬 덜 헤매게 되더라고요.
정리!
여러분도 드라이버 개발, 겁먹지 말고 하나씩 경험해보세요. 저도 아직 배우는 중이랍니다!
이제 문자 디바이스 드라이버의 핵심 기능과 실제 구현 방법을 구체적으로 알아볼까요? 이거 처음 보면 진짜 헷갈릴 수 있어요. 저도 "file_operations가 뭐지?" 하면서 한참 헤맸거든요. 그런데 구조만 이해하면 그 다음부터는 좀 수월해집니다.
문자 디바이스 드라이버(Character Device Driver)는 시리얼 포트, 키보드, 마우스처럼 바이트 단위로 데이터에 접근하는 장치에 쓰입니다. 블록 디바이스(SSD, HDD)처럼 덩어리(블록)로 데이터 읽고 쓰는 게 아니라, 한 바이트씩 차근차근 다루는 거죠.
이런 장치들로부터 데이터를 주고받으려면, read/write 연산을 잘 구현해야 해요. 이게 바로 문자 드라이버의 핵심 역할입니다.
read/write를 어떻게 구현하냐고요? 바로 file_operations 구조체를 통해서입니다.
이 구조체는 드라이버가 제공할 함수 목록을 커널에 알려주는 역할을 해요.
여러분이 /dev/mychardev
같은 디바이스 파일을 cat
이나 echo
로 읽고 쓰면, 그 동작 뒤에서 이 함수들이 호출되는 거예요!
커널 공간과 사용자 공간은 그냥 memcpy로 오갈 수 없습니다.
그래서 copy_to_user
, copy_from_user
라는 함수를 써서 데이터를 안전하게 복사해야 해요.
저는 처음에 memcpy 썼다가 커널 패닉!
실제로 이렇게 하다가 에러났었죠.
공식 함수 꼭 쓰세요. 이거 안 지키면 진짜 큰일 납니다.
진짜 간단한 예제 보여드릴게요.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#define BUF_SIZE 128
static char device_buffer[BUF_SIZE];
static int dev_major;
static struct cdev my_cdev;
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
size_t to_read = min(count, (size_t)BUF_SIZE - *f_pos);
if (copy_to_user(buf, device_buffer + *f_pos, to_read))
return -EFAULT;
*f_pos += to_read;
return to_read;
}
static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
size_t to_write = min(count, (size_t)BUF_SIZE - *f_pos);
if (copy_from_user(device_buffer + *f_pos, buf, to_write))
return -EFAULT;
*f_pos += to_write;
return to_write;
}
static struct = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
};
__init {
dev_major = register_chrdev(, , &my_fops);
(dev_major < )
dev_major;
;
}
__exit {
unregister_chrdev(dev_major, );
}
module_init(mychardev_init);
module_exit(mychardev_exit);
MODULE_LICENSE();
위 코드는 정말 최소한의 문자 디바이스 드라이버예요. read/write에서 메모리 복사 함수만 딱딱 써주고, 별다른 동기화나 인터럽트 처리는 없습니다.
조금 더 나아가서, 비동기 I/O와 인터럽트 처리 이야기도 해볼게요.
시리얼 포트처럼 데이터가 언제 들어올지 모르는 장치는 인터럽트 기반으로 처리해야 하죠.
저는 예전에 poll 구현 안 하고 select 썼다가, 앱이 블록돼서 한참 고생했어요.
비동기 지원이 필요하다면 꼭 poll/select 메서드도 구현하세요!
와, 이거 한 번에 다 외우기 힘들죠?
저도 실수하면서 조금씩 배웠으니, 여러분도 겁먹지 말고 하나씩 실습해보세요!
나중엔 "아, 이거였구나!" 하는 순간이 옵니다.
이제 블록 디바이스 드라이버의 기능과 설계에서 중요한 포인트들을 살펴볼게요.
블록 디바이스 드라이버, 이름만 들어도 좀 딱딱하죠? 저도 처음엔 "이거 진입장벽 높네" 싶었는데, 직접 만져보니 의외로 논리적이고 재밌는 부분이 많더라고요. 특히 데이터가 '고정 크기 블록' 단위로 처리된다는 점이 가장 눈에 띄어요.
블록 디바이스 드라이버는 데이터를 512바이트, 4KB 등 일정한 크기(블록)로 읽고 씁니다. 디스크(HDD), SSD, SD카드까지 모두 이 구조를 따릅니다.
예를 들어, 200바이트만 쓰려고 해도 4KB짜리 블록 전체를 읽어서 변경하고 다시 써야 해요.
"왜 이렇게 번거롭게?" 싶을 수 있지만, 덕분에 파일 시스템이 데이터 무결성을 지키고, 입출력 효율도 챙길 수 있습니다.
제가 리눅스에서 블록 디바이스 드라이버를 디버깅할 때 가장 많이 만난 개념이 바로 버퍼 캐시와 페이지 캐시였어요.
버퍼 캐시는 블록 단위 데이터를 임시로 저장해서 디스크 접근 횟수를 줄여주고, 페이지 캐시는 메모리 페이지 단위로 파일 시스템과 연동합니다.
쉽게 말하면, 자주 쓰는 데이터는 메모리에 미리 올려놨다가 빠르게 응답하는 거죠.
덕분에 "와, 이건 정말 체감상 빠르다" 싶은 순간이 오기도 합니다.
// bio 구조체를 통한 블록 I/O 요청 예시 (커널 코드 일부)
struct bio *my_bio = bio_alloc(GFP_KERNEL, 1);
bio_set_dev(my_bio, bdev);
my_bio->bi_iter.bi_sector = start_sector;
bio_add_page(my_bio, page, PAGE_SIZE, 0);
submit_bio(READ, my_bio);
실제로 이렇게 bio 구조체를 만들어 요청을 보내는데, 커널이 내부적으로 버퍼/페이지 캐시를 잘 활용해줍니다.
저는 캐시를 강제로 무시해보려고 했더니 성능이 확 떨어지는 걸 보고 "이거 진짜 중요하구나" 느꼈어요.
블록 디바이스 드라이버도 file_operations 구조체를 통해 read, write, ioctl 같은 함수 포인터를 구현합니다.
사용자 공간에서 읽기/쓰기를 요청하면, 커널이 이 함수들을 호출해서 데이터를 전달하죠.
static struct file_operations blkdev_fops = {
.owner = THIS_MODULE,
.read = blkdev_read,
.write = blkdev_write,
.unlocked_ioctl = blkdev_ioctl,
};
그런데 실제 데이터 이동은 bio 구조체와 request_queue에서 일어나요.
여러 요청이 동시에 들어오면 큐를 쌓고, I/O 스케줄러가 순서를 정해서 처리합니다.
저도 큐 관리 코드를 삽질하다가 요청 순서 꼬여서 데이터가 잘못 읽힌 적 있었어요.
"아, 큐 관리는 정말 신경 써야 하는구나"라는 걸 뼈저리게 느꼈죠.
하드디스크, SSD, SD카드 등은 각기 명령어 세트와 프로토콜이 달라서, 드라이버 설계도 그에 맞춰야 합니다.
한국에서 많이 쓰는 삼성 SSD 같은 경우도 펌웨어와 드라이버가 잘 맞아야 속도가 나오더라고요.
그리고 개발/테스트용으로 자주 쓰는 게 RAM 디스크(ramdisk), loopback 장치 같은 가상 블록 디바이스입니다.
sudo modprobe brd rd_size=8192 rd_nr=1
mkfs.ext4 /dev/ram0
mount /dev/ram0 /mnt
이렇게 하면 실제 하드웨어 없이도, 메모리를 블록 디바이스처럼 쓸 수 있어요.
실험용으로 엄청 유용하더라고요.
다만, "실수로 데이터를 날려먹지 않도록" 조심해야 해요. RAM 디스크는 휘발성이니까요.
여기까지 정리!
블록 디바이스 드라이버는 고정 크기 블록 처리, 커널 캐싱, 파일 연산 구조체, 다양한 실제/가상 디바이스 지원이 핵심입니다.
설계할 때는 효율적인 큐 관리, 캐시 활용, 그리고 해당 디바이스 특성까지 꼼꼼히 고려하세요.
저도 실수하면서 익히는 재미가 쏠쏠하니, 겁먹지 말고 도전해보시길 추천드려요!
이제 devfs와 사용자 공간 인터페이스에 대해 알아볼까요?
devfs가 뭐 하는 녀석인지, 저도 처음엔 헷갈렸어요. 한마디로, devfs는 리눅스 커널에서 하드웨어 장치(디바이스)를 사용자 공간에 노출해주는 가상 파일 시스템입니다. /dev/sda
같은 파일, 다들 한 번쯤 ls 해보셨죠? 이게 바로 devfs의 대표적인 산출물이에요. 커널에서 드라이버를 등록하면, devfs나 요즘엔 udev, devtmpfs 같은 시스템이 자동으로 /dev
밑에 장치 노드를 만들어줍니다. 예전에는 mknod로 하나씩 만들던 시절도 있었는데, 솔직히 엄청 번거로웠죠.
devfs 덕분에 사용자나 응용 프로그램이 디바이스랑 통신할 때 그냥 파일 다루듯 open, read, write, ioctl 같은 표준 파일 시스템 콜을 쓸 수 있습니다. 예를 들어 UART 시리얼 통신 드라이버를 만든다고 치면, /dev/ttyS0
를 열어서 데이터를 주고받을 수 있고, 특수한 제어 명령은 ioctl로 날리면 됩니다. 저도 예전에 가상 터미널 드라이버를 짜다가 ioctl 구현을 빠뜨려서 디바이스 제어가 안 됐던 적이 있었거든요. 이런 실수, 다들 한 번쯤 하시죠?
드라이버를 등록할 땐 register_chrdev_region
이나 alloc_chrdev_region
으로 번호를 받고, cdev
구조체를 커널에 등록하는데요. 이때 devfs가 알아서 노드를 만들어줍니다. 사용자 공간에서는 그냥 /dev/디바이스명
만 알면 되니까 얼마나 편한지 몰라요.
정리하자면, devfs는 복잡한 하드웨어 인터페이스를 사용자 친화적으로 바꿔주는 다리 역할을 해요. 드라이버 개발할 때 이 구조를 이해하면, 실제 구현이나 디버깅이 훨씬 수월해집니다. 저도 아직 배우는 중이지만, 이런 기본 원리 알면 실수도 줄고, 일할 때 한결 든든해져요!
이제 드라이버 개발에서 절대 빼놓을 수 없는 동시성, 메모리 관리, 오류 처리 세 가지 핵심 이슈를 이야기해볼게요.
캐릭터 디바이스 드라이버 처음 짤 때, 여러 프로세스가 동시에 접근하면 어떻게 될까? 저도 실제로 레이스 컨디션 때문에 진땀 뺀 적이 있어요. 다들 이런 경험 있으시죠? 예를 들어, 전역 변수로 버퍼를 하나 관리하는데, 두 스레드가 동시에 write 하면 값이 꼬여버리는 거죠.
이럴 때 사용하는 게 바로 동기화 기법입니다. 대표적으로 spinlock, mutex, seqlock, RCU 등이 있어요.
예시 코드로 볼까요?
// spinlock 예제
spinlock_t my_lock;
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
// 공유 데이터 접근
spin_unlock_irqrestore(&my_lock, flags);
// mutex 예제
struct mutex my_mutex;
mutex_lock(&my_mutex);
// 공유 데이터 접근
mutex_unlock(&my_mutex);
저도 처음에는 "도대체 언제 spinlock이고 언제 mutex인지" 헷갈렸는데, 인터럽트 컨텍스트에서는 spinlock, 프로세스 기반에서는 mutex! 이거 꼭 기억하세요.
메모리 누수… 한 번 겪으면 시스템이 뻗어버려서 완전 멘붕 옵니다.
저는 kmalloc으로 버퍼 할당해놓고, 오류 나면 해제를 깜빡해서 몇 시간 삽질한 적이 있어요. 그래서 요즘은 할당할 때마다 반드시 대응하는 kfree를 어디에 둘지 미리 계획합니다.
또, 빈번하게 할당/해제를 반복하면 슬랩 캐시(slab cache)를 직접 만들어 쓰는 것도 방법이에요.
// slab 캐시 생성 및 사용 예시
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_cache", sizeof(struct my_struct), 0, 0, NULL);
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 사용 후
kmem_cache_free(my_cache, obj);
슬랩 캐시 덕분에 성능도 오르고, 메모리 파편화도 줄일 수 있더라고요.
누수 잡으려면, 에러 처리 루트에서 꼭 할당된 메모리 해제하는 거 잊지 마세요!
커널 버전마다 API가 달라져서, #if LINUX_VERSION_CODE
이걸로 조건부 컴파일을 꽤 많이 하게 되더라고요. 예를 들어, 인터럽트 등록 함수가 바뀐다든지, 멤버 이름이 달라진다든지요. 그리고 아키텍처별로 엔디안(Endian) 처리, 주소 매핑 방식도 신경 써야 해요.
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
// 새로운 API 사용
#else
// 기존 API 사용
#endif
이런 식으로 분기 처리해주면, 나중에 "왜 이 커널에선 안 되지?" 하는 일 줄일 수 있죠.
오류 처리는 정말 꼼꼼하게 해야 해요. 함수 호출 결과는 항상 체크! 만약 실패하면, 할당된 자원 다 해제하고, 잠금도 풀고 나와야 해요.
예전에 제가 잠금 걸고 에러나서 return 했는데, unlock 안 해서 그대로 데드락 걸린 적이 있거든요. 그 뒤론 항상 goto err 패턴을 사용합니다.
buffer = kmalloc(SIZE, GFP_KERNEL);
if (!buffer) {
pr_err("메모리 할당 실패\n");
ret = -ENOMEM;
goto out;
}
// ...
out:
kfree(buffer);
return ret;
그리고 printk, dev_err 이런 로그 함수 잘 써두면, 나중에 문제 생겼을 때 원인 찾기 진짜 쉬워져요.
정리!
동시성은 상황에 맞는 락 선택, 메모리 관리는 철저한 해제와 슬랩 캐시 활용, 호환성은 조건부 컴파일, 오류 처리는 항상 체크와 롤백!
저도 실수 많이 하면서 배우는 중이니까, 같이 성장해봐요!
오늘은 문자 및 블록 디바이스 드라이버의 핵심 원리와 구현 방법, devfs와 사용자 공간 인터페이스, 그리고 동시성·메모리 관리·오류 처리와 디버깅 전략까지 폭넓게 살펴봤습니다.
이제 실제로 간단한 디바이스 드라이버를 직접 작성하고, 다양한 응용 분야에 도전해 보세요.
첫걸음이 어렵더라도, 도전하는 과정에서 성장하는 자신을 분명히 발견할 수 있을 거예요.
문자 및 블록 디바이스 드라이버는 커널 모듈로 구현되는 경우가 많으니, 커널 모듈의 구조와 작성법을 이해하는 것이 필수입니다.
블록 디바이스 드라이버는 I/O 큐잉, 버퍼링, 캐싱 등과 밀접하게 연관되어 있습니다.
디바이스 드라이버가 실제 하드웨어에 매핑되는 과정을 이해하고, 동적으로 디바이스를 관리하는 방법을 익힙니다.
문자/블록 디바이스 드라이버는 VFS와 상호작용합니다. VFS의 구조와 동작 원리를 알아두면 좋겠죠?
더 궁금한 용어나 개념이 있다면 Kernel Newbies나 LDD3에서 찾아보세요!
여러분, 이거 하다가 3시간 날린 적도 있고, 처음엔 완전 망한 적도 많아요. 하지만 그 덕분에 지금은 조금씩 자신감이 붙었답니다. 여러분도 포기하지 말고, 궁금한 점은 언제든 질문해 주세요. 우리 같이 성장해요!