수업 정리/임베디드시스템소프트웨어

[embedded system software/5주차] 수업정리

gyuun365 2026. 4. 1. 21:12

해당 글은 건국대학교 진현욱 교수님의 임베디드 시스템 소프트웨어 수업 내용을 정리한 글입니다. 

 

 

Possible Semantics of I/O Interfaces

Blocking I/O

write/read 같은 system call들을 의미하며 I/O Operation이 완료될 때까지 기다리는 행위를 의미한다.

 

Nonblocking I/O

만약 I/O Operation이 즉시 완료될 수 없다면 포기하는 행위를 의미한다.

정확히는 에러 메시지(EAGAIN 또는 EWOULDBLOCK)를 즉시 반환받는 것!

 

Asynchronous I/O

background에서 수행되는 I/O operations 들을 남기는 행위를 의미한다.

 

Semantics of Blocking Output

write() system call for disk output

write(fd, *buf, length) 라는 시스템 콜은 storage file system을 통해 이루어진다.

Return 값으로는 write에 성공한 만큼을 반환하곤 하는데, 이것은 사용된 buffer를 안전하게 reuse해도 된다는 의미이다.

 

좀 더 자세히 얘기를 해보자.

User space 에서 Write I/O가 일어나면 Disk에 바로 써질까? 

정답은 아니다 이다. 왜냐하면 시간이 너무 오래 걸리는 작업이기 때문에 Page Cache에 Copy만 해두고 바로 return을 한다.

그렇게 Page cache에 남은 데이터들은 DMA 등이 따로 시간이 날 때 Disk에 저장을 해준다는 소리다.

 

여기서 중요한 것은 buffer에 있는 데이터를 copy한다는 것인데, 이미 백업에 생겼으니 그 buffer를 재사용해도 된다는 것이였다.

더보기

추가로 덧붙이자면 만약 Page cache에 있는 채로 OS가 꺼진다면 어떻게 될까?

사실 이는 막을 수가 없다. 이를 방지하려면 fsync() 등을 써서 Page cache를 안쓰고 강제로 Disk에 밀어 넣어야 한다.

근데 저렇게 꺼진다면 파일 시스템의 일관성(Consistency) 또한 무너지게 되는데, 이것의 해결방안은 바로 Journeling이다.

일단 왜 파일 시스템이 무너지냐면 파일 하나를 저장할 때는 단순히 데이터만 쓰는 게 아니라, 파일 크기, 생성 시간, 위치 정보 같은 메타데이터, 즉 inode 쪽에 문제가 생긴다.  하지만 journeling은 실제 작업을 하기 전에 (수정중)이라는 로그를 Journel에 남겨서 불상사가 일어났을 때, journel을 확인해서 빠르게 복구가 가능하다.

write() system call for network output

이러한 문제는 disk 관련 문제 뿐만이 아니라 TCP/IP 에서도 일어나게 되는데, 

마찬가지로 Return이 됐다는 것은 그 buffer를 안전하게 재사용해도 된다는 뜻이지만, 이것은 데이터가 network를 통해 성공적으로 보내짐! 을 의미하지는 않는다는 것이다. Socket buffer/mbuf 에 Copy한 후 리턴되는 것이기 때문이다.

return != 수신자의 수신

 

read() system call for disk Input

read system call에선 page cache에 그 데이터가 있냐 없냐가 중요하다. Page cache에 있다면 바로 그 값을 리턴을 해준다.

하지만 만약 없다면 disk I/O 가 일어나면서 읽어와야하기 때문에 프로세스는 잠시 잠들어 있는다.

여기도 똑같이 return이 된다면 I/O 작업이 끝나고 데이터가 유저 공간의 버퍼에 완전히 담겼음을 보장한다. 이제 애플리케이션은 그 데이터를 안전하게 읽어서 비즈니스 로직을 수행할 수 있다는 것이다.

 

read() system call for network Input

 

네트워크 카드로 데이터(Packet)가 들어오기를 기다려야 하는 상황인데, 상대방이 데이터를 보내지 않았거나 패킷이 도착하는 중이라면, 데이터가 커널의 수신 버퍼(Receive Buffer)에 찰 때까지 프로세스는 또 잠들어 있는다.

Return의 의미는 최소한 요청한 바이트만큼(또는 그 일부라도) 패킷이 도착하여 유저 버퍼로 복사가 완료되었다는 뜻이다.

 

Implementing Blocking I/O

Wait queue

프로세스에는 3가지 상태가 있다. 실행되고 있는 Running, CPU자원만 받으면 바로 실행될 준비가 된 Ready, I/O작업 중인 Blocked 등이 있다.

이러한 Blocked 상태에 있는 프로세스들은 event가 만족될 때까지 Wait queue(Blocked queue)라는 곳에 있다.

Wait queue는 하나처럼 보이지만 여러 종류가 존재한다.

더보기

자원별 전용 큐 (Device Queue)

운영체제는 대기 원인(자원)별로 별도의 큐를 유지한다.

  • Disk I/O Wait Queue: 디스크 컨트롤러가 데이터를 다 읽어오기를 기다리는 프로세스들의 집합
  • Network Wait Queue: 랜카드(NIC)에 패킷이 도착하기를 기다리는 프로세스들의 집합
  • Keyboard/Mouse Queue: 사용자의 입력 이벤트를 기다리는 프로세스들의 집합
  • Timer Queue: sleep() 처럼 특정 시간이 지나기를 기다리는 프로세스들의 집합

Wait Queue Data Structure

Wait_queue_head_t

struct __wait_queue_head {
    spinlock_t lock;
    struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

앞서 많이 봤던 list_head가 있는 것을 볼 수 있고 spinlock까지 존재한다.

각각의 wait queue에는 doubly linked list 형태로 존재하며 위 코드에서는 wait_queue_head_t가 head가 될 것이다.

이제부터 struct __wait_queue_head 대신 wait_queue_head_t라고 써도 된다"는 뜻

 

Initialization Function

2가지 방식으로 초기화를 시킬 수 있다.

DECLARE_WAIT_QUEUE_HEAD(my_queue);

or


wait_queue_head_t my_queue;
init_waitqueue_head(my_queue);

첫번째 방식은 매크로를 이용해서 간단하게 등록이 가능하고 두번째 방식은 좀 더 복잡하지만 함수 안에도 쓸 수 있다.

Sleep Function (include/linux/wait.h)

wait_event(wq, condition),
wait_event_interruptbile(wq, condition) or
wait_event_interruptbile_timeout(wq,condition, timeout)

 

Wait_event(wq, condition)

wait_event 함수들은 앞서 만든 wait_queue를 첫번째 매개변수에 넣고 두번째에는 특이하게도 조건을 넣어주면된다.

 

Wait_event_interruptbile(wq, condition)

위 함수와 다른점은 그저 condition 뿐만 아니라 interrupt로도 깨어날 수 있게 해주는 함수이다.

 

Wait_event_interruptbile_timeout(wq, condition, timeout)

위와 똑같지만 timeout 기능을 추가로 넣어서 시간이 지난다면 자동으로 깨게 해주는 함수이다.

 

그래서 2,3번째 함수는 만약 interrupt가 온다면 "-ERSTARTSYS" 에러메세지를 반환한다.

Wake-up Function 

이 함수에는 wake_up(&wq) or wake_up_interruptible(&wq) 들이 존재하는데 2개는 별차이 없다고 한다.

위에서 잠들었던 프로세스들을 깨우는 함수들이라고 보면 되는데, 주의사항으로 잠든 프로세스 안에 이 친구들을 넣지는 말아야한다.

무조건 잠든 프로세스와 겹치지 않게 부르는 것에 주의하자

 

Example

static DECLARE_WAIT_QUEUE_HEAD(wq);
static int flag = 0;

ssize_t sleepy_read (struct file *filp, char __user *buf,size_t count, loff_t *pos)
{
	
    printk(KERN_DEBUG "process %i (%s) is going to sleep\n",current->pid, current->comm);
    wait_event_interruptible(wq, flag != 0);
    flag = 0;

    printk(KERN_DEBUG "awoken %i (%s)\n",
    current->pid, current->comm);
    return 0;
}

 

static DECLARE_WAIT_QUEUE_HEAD(wq);

여기선 매크로로 초기화를 먼저 해준 뒤

 

static int flag = 0;

전역 변수인 flag를 선언해준다.

 

첫 번째 printk의 current는 Running state에 있는,즉 현재 코어인 프로세스를 의미한다.

 

wait_event_interruptible(wq, flag != 0);

초기화 시켜준 wq와 condition이 왔는데, 의아해할 것은 ""도 아니고 if문 안에 들어가야할 문장 그대로 두번째 파라미터에 들어갔다는 것이다. 사실 저 함수를 까보면 매크로 함수로서 저 2번째 매개변수는 if문 안에 쏙 들어가는 것을 볼 수 있다.

 

어쨌든 flag!=0이면 깨어난다는 소리이다. 지금은 0이므로 성공적으로 wait 상태에 들어갈 것이다.

깨어난 후, flag = 0; 을 기입해준다.

 

 

ssize_t sleepy_write (struct file *filp,const char __user *buf, size_t count,loff_t *pos)
{
    printk(KERN_DEBUG "process %i (%s) awakening the readers...\n", current->pid, current->comm);
    flag = 1;
    wake_up_interruptible(&wq);
    return count;
}

flag를 1로 바꿔준 후 wake_upinterruptible로 깨워주는 것을 볼 수 있다.

 

count는 그냥 return해줘야하니까 함

 

여기서 read() -> write() 순으로 실행되면 sleep했다가 성공적으로 깨어 나겠지만

역순으로 진행이 된다면 flag가 1이므로 그저 통과만 할 것이다.

 

그래서 flag 개념으로는 race condition이라고 보지 않지만 count의 개념이면 race condition으로 이 코드를 볼 것이다.

Exclusiveness

이기적이다. 사실 주관적인 느낌일 것이다. 스케줄링 시스템도 오랫동안 고민해온 주제이기도 하다. 공평하다는 것은 무엇일까?

 

위의 wakeup들은 그러면 여러개가 잠들어있을때 누굴 먼저 깨워야 공평할까? 

예전 스케줄링은 CFS를 사용하고 요즘은 EEVDF를 사용한다고 한다. 둘의 공통점은 CPU를 사용하는 시간이라고 한다.

Nonexclusive process

위에서 설명했던 wait 함수들을 기억하는가

  • wait_event(wq, condition)
  • wait_event_interruptbile(wq, condition)
  • wait_event_interruptbile_timeout(wq,condition, timeout)

이 함수들은 발동되면 해당 Wait Queue를 확인했을 때, 그 줄에 서 있는 모든 Nonexclusive 프로세스들을 전부 다 깨워서 Ready 상태로 만든다.

Exclusive process

wait_event_interruptbile_exclusive() 

이 함수들은 발동되면 해당 Wait Queue를 확인했을 때, 한 프로세스만 깨워서 Ready 상태로 만든다.

 

#define WQ_FLAG_EXCLUSIVE 0x01
struct __wait_queue {
    unsigned int flags;
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;

Nonexclusive와 exclusive 들로 이루어진 함수들은 위 코드에서 flags 부분만 다르다. flags 부분이 Nex or Ex로 각각 다른 flag를 갖는다. 

 

Nonexclusive process & Exclusive process

그렇다면 저 함수들을 섞어쓸 수 있을까?

두 개를 섞어 쓰게 된다면 다음과 같은 모양으로 나온다. NonExclusive는 head쪽에 Exclusive는 tail 쪽에 쓰이게 되서 만약 깨운다면 
Nex는 다 일어나게 되고 Ex 하나를 만나면 거기까지만 깨우고 멈추게 된다.