해당 글은 건국대학교 진현욱 교수님의 임베디드 시스템 소프트웨어 수업 내용을 정리한 글입니다.
Exported Kernel Symbols
static이랑은 정 반대로 다른 모듈의 함수나 변수를 쓸 수 있게 하기 위한 symbol들이라고 생각하면 된다.
"/proc/kallsyms"에 정의되어 있으며 cat /proc/kallsyms을 치면 엄청난 양의 목록이 나온다.
- EXPORT_SYMBOL(): 모든 모듈에 심볼을 공개한다.
- EXPORT_SYMBOL_GPL(): GPL 라이선스를 따르는 모듈에게만 공개하는 더 까다로운 방식이다.
User Memory Access
copy_to_user(void __user *to, const void *from, unsigned long n)
Kernel space -> User space로 데이터를 전달할 때 사용한다.
유저에게(to_user) 커널(from)에 있는 걸 건네준다 라고 외우면 좋을듯 싶다.
copy_from_user(void *to, const void __user *from, unsigned long n)
User space -> Kernel space 로 데이터를 받을 때 사용한다.
유저로부터(from_user) 가져와서 커널(to)에 넣는다 라고 외우자
Const : 복사할 원본 데이터(Source)가 실수로라도 변하지 않도록 위함.
__user 매크로: 인자에 붙은 이 표시는 "이 주소는 유저 영역꺼니까 함부로 직접 참조(*ptr)하지 마!"라고 커널에게 경고하는 일종의 안전장치이다. 그래서 반드시 이 함수들을 거쳐야만 안전하게 데이터를 가져올 수 있어서 이용하는 것.
예를 들어
int *p;
p = 0X100;
*p = 7
이라는 코드가 있다고 가정했을 때 보통 실행할 때 오류가 날 것이다.
이 주소(0x100)에 대응하는 실제 RAM 공간이 정의되어 있지 않아서 page fault 등의 문제가 있을 것이다.
하지만 kernel module에서 저 코드는 오류가 안날지도 모른다. 커널 모드이여서 접근이 가능해지기 때문인데, 만약 저 주소가 시스템 부팅에 중요한 데이터가 들어있는 주소였다던가 등의 사고가 일어나면 안되기 때문에 위에 두 함수들을 설명해준것이다.
두 함수의 매개변수를 보면 주소가 인자값인걸 볼 수 있다. 왜냐하면 저 함수 안에는 주소가 안전한 주소인지 검증해주는 주소 검증의 로직도 들어가 있기 때문에다.
만약 code Segment등의 유저 버퍼가 보낸 주소가 read, write가 가능한 곳의 주소가 아니였다면
-EFAULT 에러를 반환해주고
만약 포인터가 kernel memory를 침범했다면 안전상의 이유로도
-EFAULT 에러를 반환해줄 것이다.
1. get_user(...) & put_user(...)
이 둘은 단일 변수(Simple variable) 전용.
- get_user: 유저 공간에 있는 int, char 같은 작은 값 하나를 읽어온다.
- put_user: 커널에 있는 값 하나를 유저 공간의 변수에 쓴다.
- 왜 쓰나요? copy_from_user는 오버헤드가 조금 있는데, 딱 숫자 하나만 옮길 때는 이 함수들이 훨씬 가볍고 빠르다.
(단, 구조체나 배열처럼 큰 데이터는 불가)
2. clear_user(...)
- 역할: 유저 공간의 특정 메모리 블록을 모두 0으로 채운다. (C언어의 memset(ptr, 0, size)과 비슷함.)
- 언제 쓰나요? 유저에게 넘겨줄 버퍼를 깨끗하게 초기화하거나, 보안상 데이터를 지워야 할 때 사용한다.
3. strlen_user(...) & strncpy_from_user(...)
이 둘은 문자열(String) 처리에 특화되어 있는데, 유저가 준 문자열은 어디가 끝(\0)인지 커널이 미리 알 수 없기 때문에 전용 함수가 필요하다.
- strlen_user: 유저 공간에 있는 문자열의 길이를 안전하게 잰다.
- strncpy_from_user: 유저 공간의 문자열을 커널 버퍼로 복사한다.
- 특징: 지정한 크기를 넘지 않게 복사하며, 복사 중에 유저 메모리 영역을 벗어나는지 등을 실시간으로 체크한다.
Memory Allocation
메모리 할당에는 vmalloc/vfree 와 kmalloc/kfree 가 있는데,
Virtual Memory 에서 Page directory, page table을 거쳐 physical memory 로 가는 흐름에서
Page table -> Physical Memory로 갈 때 linear 하게 저장되냐 아니냐의 차이가 있다.
vmalloc / vfree
인자로는 void *vmalloc(unsigned long size)만 갖고
위에서 말한 것 중에 Page table -> Physical Memory로 갈 때 non-linear 하다.
이것을 not consecutive 하다 라고도 말한다. 뒤에서 말하겠지만 그런 특징 때문에 DMA를 위한 메모리 할당에 적합하지 않다.
kmalloc / kfree
인자로 void *kmalloc(size_t size, int flags)를 갖고
위에서 말한 것 중에 Page table -> Physical Memory로 갈 때 linear 하다.
flag
- GFP_KERNEL (가장 일반적)
- 특징: 메모리가 부족하면 확보될 때까지 잠들 수(Sleep) 있다.
- 용도: 프로세스 컨텍스트(시스템 콜 처리 등)에서 사용한다. "조금 기다려도 되니 꼭 메모리를 할당해 줘"라는 뜻
- GFP_ATOMIC (긴급 상황)
- 특징: 절대 잠들지 않는다. 메모리가 없으면 기다리지 않고 즉시 실패(NULL)를 반환한다.
- 용도: 인터럽트 핸들러(Interrupt Handler)처럼 1분 1초가 급하고 절대 멈추면 안 되는 곳에서 사용한다.
- GFP_DMA (하드웨어용)
- 특징: 아주 낮은 물리 주소 영역(보통 16MB 이하)에서 메모리를 할당한다.
- 용도: 구형 하드웨어나 특정 DMA 장치가 낮은 주소만 인식할 때 사용한다.
- DMA를 위한 메모리 할당에 유리한 이유는 하드웨어가 DMA(Direct Memory Access)를 통해 직접 메모리에 접근하고 하드웨어는 페이지 테이블을 거치지 않고 실제 RAM 주소를 따라가기 때문에 중간에 끊기지 않은 연속된 공간이 필요하기 때문이다.
- DMA는 가상 메모리에 대해 몰라서 시작주소랑 길이만 정해주면 되기 때문에 낮은 물리 주소 영역에서 보통 사용된다.
Linked Lists
리눅스 커널은 또한 circular, doubly linked list 프레임 워크를 제공한다.
하지만 locking이 구현되어 있지 않아서 우리가 알아서 구현해야한다는 단점

그림처럼 이 list_head는 위치에 상관이 없다. 보통의 경우에는 맨 위나 아래에 존재하는데 중간에 존재해도 됨!
struct list_head {
struct list_head *next, *prev;
};
이런식으로 보통 list_head를 구현할 수 있으며,
struct my_node {
...
struct list_head list;
int my_data1;
int my_data2;
}
저렇게 코드 중간에 와도 크게 상관이 없다!
header 를 초기화 시키는 것은 void INIT_LIST_HEAD(struct list_head *list) 를 사용한다.
Linked Lists - Add
void list_add( struct list_head *new, struct list_head *head)
노드를 가장 앞에 추가하는 것으로 새로운 노드를 head의 바로 다음에 추가한다.
만약 이미 head와 A라는 노드가 있었다면 순서는 head - new - A 가 되는 것이다.
void list_add_tail( struct list_head *new, struct list_head *head)
노드를 가장 뒤에 추가하는 것으로 새로운 노드를 head의 바로 앞(prev)에 추가한다.
이럴 수 있는 이유는 리눅스의 리스트는 circular이기 때문이다.
이렇기 때문에 가장 뒤에 추가한다고 하더라도 속도가 빠른 것을 볼 수 있다.
Linked Lists - Deletion
list_del( struct list_head *entry)
노드를 삭제하는 함수로서 정확히 말하자면 entry 노드의 prev와 next를 서로 연결시켜서, entry를 리스트 흐름에서 고립시킨다.
문제는 이 entry 노드는 아직도 옛날 값들을 갖고 있을 수도 있어서 list_add 등의 행동을 하려할 때 오류가 날수도 있다.
list_del_init( struct list_head *entry)
그래서 이 함수는 위의 문제를 해결하기 위해 그 전까지는 똑같지만 추가로 해당 entry를 INIT_LIST_HEAD 상태로 만든다.
Linked Lists - Iteration
사실 이제 제일 헷갈리는 부분인데, list_for_each(pos, head) 를 이용한다.
for 문을 생각하면 되며 매크로 함수이다. 예시를 바로 보자

struct list_head *pos;
struct my_node *entry;
list_for_each(pos, &my_list){
entry = list_entry(pos, struct my_node, list);
printk(“Data1: %d\n”, entry->my_data1);
printk(“Data2: %d\n”, entry->my_data2);
}
pos
for문의 Index로 생각하면 되고 차이점은 포인터로 미리 선언해야한다는 점이다. 그래서 for 문처럼 한바퀴 돌 때마다 그 다음 노드를 가리키게 된다. 내부적으로는 for (pos = (head)->next; pos != (head); pos = pos->next) 처럼 동작한다.(circular이기 때문에)
&my_list
여기엔 list의 head를 넣으면 된다. 그림처럼 my_list의 주소값!
list_entry
pos만 가지고는 my_node 안에 있는 my_data1 같은 진짜 데이터에 접근할 수 없다.
pos는 그저 연결 고리이기 때문에, 이때 사용하는 마법이 list_entry이다.
- pos: 현재 보고 있는 고리 주소
- struct my_node: 찾아야 할 구조체 타입
- list: 아래 그림처럼 그 구조체 안에서 list_head의 이름

list_for _each_safe(pos, n, head)
만약 리스트를 순회하는 도중에 현재 노드를 삭제(delete)해야 할 때, list_for_each를 이용한다면 pos 의 next가 끊겨버려서 이도저도 못하는 상황에 갇혀버리고 만다.
그래서 n이라는 임시 저장 장소에 보관해놨다가 next로 길을 이어주는 것이 바로 이 함수이다.
Spinlock
아까 리눅스의 linked list에는 locking이 따로 없다고 하였는데 그래서 이 synchronization을 할 때 간단하게 spinlock을 사용해서 해보겠다
busy waiting 방식이며
- spinlock_t
- spin_lock()
- spin_unlock()
#include <linux/spinlock.h>
static DEFINE_SPINLOCK(my_lock);
void my_cs(void)
{
spin_lock(&my_lock);
/* critical section */
spin_unlock(&my_lock);
}
#include <linux/spinlock.h>
struct my_val {
spinlock_t lock;
int value;
} mv;
void mv_init(void)
{
spin_lock_init(&mv.lock);
mv.value = 0;
}
void my_cs(struct my_val *v)
{
spin_lock(&v->lock);
/* critical section */
spin_unlock(&v->lock);
}
위쪽은 전역 변수처럼 고정된 락을 쓸 때, 아래쪽은 구조체 안에 포함시켜 동적으로 관리할 때 사용된다고 할 수 있다.
정적 할당 (Static Allocation)
static DEFINE_SPINLOCK(my_lock);
- 방식: 컴파일 타임에 my_lock이라는 이름의 스핀락 변수를 만들고, 즉시 사용할 수 있게 초기화까지 마친다.
- 특징:
- 간편함: 따로 초기화 함수(spin_lock_init)를 호출할 필요가 없다. 선언하자마자 바로 spin_lock()을 쓸 수 있다.
- 범위: 주로 특정 모듈 전체에서 공통으로 사용하는 전역적인 자원을 보호할 때 사용.
동적 할당/구조체 포함 (Dynamic/Struct Inclusion)
spinlock_t lock; + spin_lock_init(&mv.lock);
- 방식: 먼저 구조체 멤버로 스핀락을 정의하고, 런타임(프로그램 실행 중)에 spin_lock_init() 함수를 호출하여 초기화합니다.
- 특징:
- 유연함: 구조체 인스턴스가 여러 개 생성될 때(예: 여러 개의 장치, 여러 명의 유저 등), 각 객체마다 독립적인 락을 가질 수 있다.
- 절차: 메모리만 할당받는다고 끝나는 게 아니라, 반드시 init 함수를 거쳐야 락이 정상 작동합니다. 초기화를 안 하고 쓰면 커널이 죽는다. (별도 선언 필수!!)
정적할당의 매크로는 구조체 안에 못넣는다는 것도 알고 가면 좋다.
Adding Delay
- udelay(usecs): 마이크로초($\mu s$, 100만분의 1초) 단위 대기.
- mdelay(msecs): 밀리초($ms$, 1000분의 1초) 단위 대기.
Busy-Waiting 방식으로 CPU가 그냥 대기하는 방식
- msleep(msecs): 지정된 밀리초만큼 프로세스를 재운다.
Sleep-Waiting 방식으로 유저가 보낸 요청을 처리하는 중에 쓸 수 있다.
interrupt handler 사용 불가!!!! 절대 넣으면 안됨
- schedule_timeout(): 현재 프로세스를 특정 시간(jiffies) 동안 휴면 상태로 만든다. 가장 기본적인 형태의 수면 함수이다.
- wait_event_timeout():
- 조건 기반: "특정 변수값이 참(true)이 되거나, 아니면 5초가 지날 때까지 자겠다"는 뜻.
- 데이터가 도착하기를 기다리는데, 혹시라도 안 올 경우를 대비해 '타임아웃'을 걸어두는 용도로 실무에서 아주 많이 쓴다.
그래서 주기적으로 timer를 설정해서 OS가 간섭하는 등으로 쓴다.
만약 linux kernel source code 가 보고 싶다면 (X.X.X 는 버전을 의미)
Linux Kernel Source Codes: https://elixir.bootlin.com/linux/vX.X.X/source
'수업 정리 > 임베디드시스템소프트웨어' 카테고리의 다른 글
| [embedded system software/9주차] 이론 (0) | 2026.04.29 |
|---|---|
| [embedded system software/6주차] 이론 (0) | 2026.04.15 |
| [embedded system software/5주차] 수업정리 (0) | 2026.04.01 |
| [embedded system software/3주차] Character Device Drivers (0) | 2026.03.21 |
| [embedded system software/2주차] Loadable Kernel Modules (1) | 2026.03.16 |