DEVELOP

임베디드 시스템 소프트웨어 과목의 강의영상과 강의자료를 바탕으로 작성한 학습용 게시글입니다.


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

초기화 하는 방법 

  1. 매크로 함수
    DECLARE_WAIT_QUEUE_HEAD()
    : 선언 & 초기화 동시에 할 때 
  2. 일반 함수
    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를 깨우고 종료한다. 
  • else if) B가 먼저 실행
    • flag=1이므로 깨우라고 하지만 wq에 잠들어 있는 p가 없으므로 깨울 것 없이 종료된다.
      이후 A가 실행되어도 falg=1이므로 잠들지 않고 깨어난다. 

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, exclusiveret, 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 프로세스를 깨우게 된다.

 

profile

DEVELOP

@JUNGY00N