임베디드 시스템 소프트웨어 과목의 강의영상과 강의자료를 바탕으로 작성한 학습용 게시글입니다.
Possible Semantics of I/O Interfaces
Blocking I/O
- write() , read() 시스템 콜을 쓸 때 동작하는 방식이다.
- I/O를 당장 할 수 없을 때는 해당 system call을 호출한 프로세스를 blocking state로 전이하고,
CPU 자원을 쓰지 않고 대기한다. - I/O 완전히 끝났을 때만 해당 system call을 리턴한다.
- receive 요청 받지 못하면 blocking한다.
- send할 때 버퍼 꽉 차는 경우는 데이터 넣을 수 없으므로 blocking한다.
Nonblocking I/O
- blocking하지 않는다.
- blocking이 필요하면 오류값 리턴 후 진행하지 않는다.
- 다시 시도하거나 한동안 다른 일 하다가 retry한다.
Asynchronous I/O
- 지금 당장 해결할 수 없을 경우 일단은 리턴하지만, 기억하고 있다가 해결할 수 있을 때 백그라운드로 진행
- I/O를 요청하기 위한 send/receive, read/write 외에도 I/O가 정말 끝났는 지 확인하기 위한 API가 별도로 존재한다.
- 파일 I/O는 리눅스 커널 내에 구현되어 있지만,
네트워크는 OS 내에서는 구현이 어렵고, 일종의 라이브러리 레벨에서 구현되어 있다. - 기존에 Blocking I/O 또는 Nonblocking I/O로 구현된 것은 Asynchronous I/O로 바꾸기 위해서는
꽤 다른 형태로 수정해주어야 한다.
Semantics of Blocking Output
write() System Call for Disk Output
- 파일 시스템을 통과하면서 Disk에 Block data를 쓰려고 할 것이다.
- return의 의미
: write system call에 들어있는 포인터가 가리키고 있는 버퍼를 리턴 후에 마음대로 안전하게 써도 된다.
(다른 값으로 채우거나 free해도 안전하다) - 버퍼에 있는 내용은 커널 안에 있는 별도의 버퍼에 복사를 하고 리턴을 하게 된다.
응용프로그램에서 할당한 버퍼에 대해서는 다른 값으로 overwrite하거나 free했다고 하더라도 커널 안에 그 데이터를 저장해 놓았기 때문에 안전하게 써도 된다. - 매번 write 요청이 있을 때마다 매번 Disk에 쓰고 리턴을 하게 되면 write system의 오버헤드가 너무 크기 때문에
OS쪽에서는 최대한 메모리에 그대로 두었다가 필요할 때 디스크 스케줄링을 통해 성능을 높이는 작업을 한다. - read한 data도 한시적으로 버퍼캐시(페이지 캐시)에 보관한다.
- write했다고 해서 바로 Disk에 쓰지 않고, 버퍼캐시(페이지캐시)에 넣어놓고, OS에게는 "썼다."고 리턴을 해준다.
(갑자기 전원이 꺼지면 써지지 않는다.)
write() System Call for Network Output
- Socket을 통해 얻은 descriptor를 write의 첫번째 인자값으로 넘겨주면,
write system은 Network 쪽으로 output을 하게 되어있다. - TCP/IP 계층을 통해 최종적으로 네트워크 디바이스에 dataf를 send하라고 요청한다.
- return의 의미
: free하거나 over write해도 된다는 것을 보장한다.
실제 네트워크로 보내졌다는 것을 의미하지는 않는다.
TCP/IP를 reliability 보장된다고 생각하기 쉽지만,
단지 커널 안으로 보낼 데이터를 copy란 후에 네트워크로 나가지 않았음에도 불구하고,
그 버퍼를 자유롭게 써도 된다는 것을 의미한다. - 커널 안에 저장해 놓는 버퍼 : Socket buffer, 리눅스 소켓 버퍼 : mbuf
read() System Call for Disk Input
- 먼저, 해당 데이터가 버퍼 캐시에 있는지 확인한다.
(버퍼 캐시는 OS가 관리하는 메모리 영역으로서, 자주 사용하는 데이터를 저장해놓는 메모리 공간이다.)- 있으면, 바로 read system call과 함께 전달된 버퍼 영역에 copy한다.
- 없으면, 디스크에 직접 접근하는데, 디스크가 느리기 때문에 프로세스를 재운다. (running -> waiting)
- 리턴의 의미 (리턴값이 errcode가 아니고 읽어들인 length값일 때)
: 버퍼에 당신이 원하는 데이터가 있으니, 마음대로 접근해도 된다.
read() System Call for Network Input
- system call이 네트워크로부터 수신된 패킷 메시지를 달라는 것을 의미한다.
- 만약 수신된 패킷이 없다면,
새 패킷이 도착할 때 까지 대기한다.
(프로세스 잠듦, running->waiting으로 전이하고, CPU를 다른 프로세스에게 양보한다.) - return의 의미
: 전달된 버퍼 영역에 수신된 메시지가 들어있으니, 응용프로그램이 원하는 대로 사용해도 된다는 의미이다.
Implementing Blocking I/O
Wait Queue
: 잠자고 있는 프로세스들이 모여있는 장소
- 특정 조건이 true가 되면 커널이 깨워준다.
- 지금 당장 수행이 불가할 경우, running->blocked로 상태 전이가 되고,
할당된 CPU는 스케줄링에 의해 ready 상태의 다른 프로세스에게 양보된다.
wait_queue_head_t
: wait queue의 타입
struct __wait_queue_head {
spinlock_t lock; // 동기화 위해 spinlock
struct list_head task_list; // 잠들고 있는 프로세스 리스트
};
typedef struct __wait_queue_head wait_queue_head_t;
|
- wait queue를 위한 함수들은 spinlock을 통해 lock을 acquire하고, 리스트에 대한 조작이 끝났을 경우 lock을 release하고, critical section을 떠나게 된다.
Initialization
초기화 하는 방법
- 매크로 함수
DECLARE_WAIT_QUEUE_HEAD()
: 선언 & 초기화 동시에 할 때 - 일반 함수
init_waitqueue_head()
: 선언 & 초기화를 구분하고 싶을 때
DECLARE_WAIT_QUEUE_HEAD(my_queue); or
wait_queue_head_t my_queue;
init_waitqueue_head(my_queue);
|
Sleep Functions
(include/linux/wait.h)
: 프로세스를 재우는 함수들
wait_event(wq, condition) |
- wq : 생성된 wait queue 이름
- condition : 깨어날 조건
wait_event_interruptbile(wq, condition) |
- condition을 만족하거나 signal을 받았을 때 깨어나겠다.
- 종료까지 block일 경우 재호출한다.
- error code가 리턴되면 응용프로그램으로 error code와 함께 리턴된다.
- signal에 의해 리턴되어서 nonzero값을 리턴하는 경우에는,
함수를 호출한 상위 함수에게 -ERSTARTSYS 를 리턴한다.
wait_event_interruptbile_timeout(wq, condition, timeout)
|
- 원하는 condition을 만족하거나 signal을 받거나 명시한 time이 만료되면 리턴한다.
Wake-Up Functions
: 프로세스를 깨우는 함수들
- 잠드는 프로세스 내에서 구현하지 않도록 주의해야 한다.
wake_up(&wq) |
- 프로세스를 blocked->ready
스케줄링에 의해 선택되면 running (바로 당장 running x) - 특정 프로세스를 깨우는 것이 아니고, wait queue를 명시했을 때 해당 wait queue 안에 있는 프로세스를 깨운다.
wake_up_interruptible(&wq) |
- 내부적으로 하는 일에는 큰 차이가 없다.
- wait 함수와 규칙을 맞춰 쌍으로 호출하는 것이 일반적이다.
Example - Wait & Wakeup
- 커널 모듈 안에 작성되었다고 가정,
커널 모듈 코드 전체x,
캐릭터 디바이스 드라이버를 위한 file operations 중에 read/write를 위한 body 부분만 작성
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;
}
|
- read 함수 내에서는 프로세스가 무조건 잠들도록 하고,
write 함수에서는 잠들었던 프로세스를 깨워주도록 한다. - current
: 현재 실행되고 있는 프로세스의 pcb = read함수를 호출한 프로세스
- 리눅스 커널 내 선언되어 있는 전역변수 , expose가 되어있기 때문에 커널 모듈에서 사용 가능
- comm
: command line에서 입력했던 이름
- file operations 중에 write에 해당하는 function pointer와 연결되는 함수의 body 부분
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;
}
|
- read를 호출하는 프로세스 A와 write를 호출하는 프로세스 B가 있을 때
- if) A가 먼저 실행
- flag=0으로 초기화 됐었기 때문에 read 함수에 의해 A가 잠이 들고,
B가 실행되면서 write를 호출하면 A를 깨우고 종료한다.
- flag=0으로 초기화 됐었기 때문에 read 함수에 의해 A가 잠이 들고,
- else if) B가 먼저 실행
- flag=1이므로 깨우라고 하지만 wq에 잠들어 있는 p가 없으므로 깨울 것 없이 종료된다.
이후 A가 실행되어도 falg=1이므로 잠들지 않고 깨어난다.
- flag=1이므로 깨우라고 하지만 wq에 잠들어 있는 p가 없으므로 깨울 것 없이 종료된다.
Exclusiveness
- wake up할 때 특정 프로세스를 깨우라고 명시하지 않고, wait queue만 명시한다.
Nonexclusive Process 방식
- 모든 프로세스를 깨워 ready state에 가도록 한다.
- CPU 스케줄링 알고리즘에 의해 하나씩 선택하면서 CPU 자원을 준다.
- 모두 똑같은 stack condition 갖고 있었다면, 스케줄링 알고리즘에 의해 가장 먼저 선택된 프로세스가 running state로 전이
- condition 조건을 다시 확인했을 때, running state로 전이된 프로세스가 falg=0으로 만들게 되면 선택받지 않은 나머지 프로세스들은 다시 잠들게 된다.
wait_event(),
wait_event_interruptbile(),
wait_event_interruptbile_timeout()
|
위의 세 함수는 아래의 함수를 부르게 되어있고, exclusive 값을 0으로 전달하게 되어있다.(=nonexclusive)
=> 깨어날 때 모두 깨어나고, CPU 스케줄러에 의해 선택된 것만 계속해서 wake up하고 실행된다.
___wait_event(wq, condition, state, exclusive, ret, cmd) |
#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;
|
- wait_queue_head_t
: head 역할 - flags
: exclusive / nonexclusive 판단- 하나의 queue 안에 exclusive / nonexclusive가 섞여있을 수 있다.
Exclusive Process 방식
: 모든 프로세스를 깨우는 것이 아니라 해당 queue에서 가장 오랫동안 기다렸던 프로세스 하나만을 깨운다.
- 프로세스를 재울 때 exclusive하다는 것을 아래 함수를 통해 명시한다.
wait_event_interruptbile_exclusive() |
하나의 queue에 Exclusive와 Nonexclusive가 섞여있는 경우
- 섞여있는 것이 일반적인 것은 고민해봐야 하지만, 다뤄볼 필요가 있다.
- exclusive 는 wait queue의 맨 뒤에 넣는다.
- nonexclusive는 wait queue의 맨 앞에 넣는다.
- wake up 함수는 exclusive/nonexclusive의 구분이 없다.
- Nonexclusive 프로세스를 모두 깨우고, 마지막에 exclusive 프로세스 딱 하나를 깨우고 더 이상 다른 프로세스를 깨우지 않는다.
- 첫번째 exclusive (가장 오래 기다린) 프로세스가 보이기 전 모든 프로세스를 깨우다보면 모든 nonexclusive 프로세스를 깨우게 된다.
'LECTURE > [2023-1] 임베디드시스템소프트웨어' 카테고리의 다른 글
[임베디드시스템소프트웨어] 05. General Purpose I/O (0) | 2023.04.19 |
---|---|
[임베디드시스템소프트웨어] 03. Basic Kernel Functions (0) | 2023.04.11 |
[임베디드시스템소프트웨어] 02. Character Device Drivers (캐릭터 디바이스 드라이버) (0) | 2023.04.10 |
[임베디드시스템소프트웨어] 01. Loadable Kernel Modules (적재 가능 커널 모듈) (0) | 2023.04.09 |