Thread

개요

  • 쓰레드란?
    • 프로세스 내의 실행 유닛
    • 프로세스의 태두리 내에서 실행되며 같은 프로세스 내의 쓰래드간 코드와 전역 변수를 공유한다.
    • 쓰레드는 상대적으로 프로세스보다 서로 데이터를 공유하는 것이 쉽다.
    • 일반적으로 프로세스보다 더 효율적이다.
  • 쓰레드간 메모리 공유
    • user stack 은 공유하지 않음
      • 함수 호출시 함수의 데이터가 쌓이는 메모리(지역변수 등)
    • 힙 메모리를 공유
      • 동적할당 메모리
    • static data 영역 공유
      • 전역변수, static 변수
    • 코드영역 공유
      • 프로그램의 코드 (함수, 제어문, 상수)

관련 함수들

  • Thread 생성 함수: pthread_create()

      int pthread_create(
              pthread_t *thread,          // 생성된 쓰레드 id 저장할 포인터
              const pthread_attr_t *attr, // attribution 뒤에 설명
              void *(*start)(void *),     //쓰레드가 main으로 사용할 함수
              void *arg                   // main함수에 전달할 데이터의 포인터
      )
    
    • return: 생성 성공시 0 실패시 error code
    • *thread: 해당 프로세스 내에서 유효하게 사용할 수 있는 쓰레드 주소를 전달받을 수 있다.
    • *attr: 생성할 쓰레드의 속성값을 명시할 수 있도록 하는 변수이다.
      • pthread_attr_t 이라는 타입(구조체)는 다음의 방법으로 생성, 조작 가능
        • 생성: int pthread_attr_init(pthread_attr_t *tattr)
        • 구조체 구성요소
          • Detach state 초깃값: PTHREAD_CREATE_JOINABLE = joinable thread를 생성 pthread_attr_setdetachstate() = detatch로 변경 가능
          • Scope 초깃값: PTHREAD_SCOPE_SYSTEM = cpu자원 분배시 시스템 내 모든 쓰레드와 경쟁 pthread_attr_setscope() = 프로세스 내 쓰레드와 경쟁하게 할 수 있음
          • Inherit scheduler 초깃값: PTHREAD_INHERIT_SCHED: 자신을 생성한 쓰레드의 스케줄링 값을 상속 받음 pthread_attr_setinheritsched()
          • Scheduling policy 초깃값: SCHED_OTHER: 새로운 쓰레드는 표준 스케줄링 규칙을 적용받음 pthread_attr_setschedpolicy()
          • Scheduling priority
          • Guard size
          • Stack size
  • 자신의 tid를 알려주는 함수 pthread_self()

      pthread_t pthread_self()
    
    • 프로세스 내에서 유효한 자신의 tid를 반환한다.
  • 자신의 Thread를 종료하는 함수: pthread_exit()

      void pthread_exit(void *retval)
    
    • 함수를 호출한 쓰레드를 종료시킨다.
    • retval을 pthread_join()을 호출한 함수에 전달한다.
      • pthread_join은 쓰레드의 잔해를 수습하고, retval을 전달받는 함수이다.
  • 다른 Thread 종료시키는 함수: pthread_cancle()

      int pthread_cancel(pthread_t thread)
    
    • thread에 쓰레드의 id (프로세스 내에서 유효한)을 넘겨주면 해당 쓰레드를 종료시킨다.
  • 다른 Thread 의 종료를 기다리고 reap하는 함수: pthread_join()

      int pthread_join(pthread_t thread, void **value_ptr)
    
    • 쓰레드의 종료까지 기다리다가(그동안 block) 종료되면 reaping
    • return: 성공시 0, 실패시 error code
    • thread: reap할 쓰레드 id를 전달
    • value_ptr: 종료 상태를 전달받을 포인터
  • Thread에 signale을 보내는 함수: pthread_kill()

      int pthread_kill(pthread_t thread, sig_t signal)
    
    • thread에 signal을 전송하는 함수

Thread 종류 (joinable)

  • Joinable thread (default)
    • 다른 쓰레드가 reap할 수 있고, 다른 쓰레드에 의해 reap 될 수 있다.
    • 다른 쓰레드가 reap하기 전까지는 메모리가 해제되지 않는다.
  • detached thread
    • 다른 쓰레드가 reap이나 종료할 수 없다.
    • 쓰레드 종료시 자동적으로 메모리가 해제된다.
    • int pthread_detach(pthread_t thread) 로 detach할 수 있다.

Thread Synchronization

  • Mutex

개요

  • 쓰레드간에 전역변수를 공유하기 때문에 전역변수에 동시에 접근할 경우 충돌이 발생할 수 있다.
  • Critical section
    • 공유되는 메모리에 접근하는 코드를 명칭
    • 같은 메모리를 두고 경합하는 critical section은 반드시 여러 쓰레드에서 동시에 실행되지 않게 해야 한다.
  • Mutual exclusion
    • Critical section에 대한 독점권을 부여하는 것
    • 어떤 쓰레드가 critical section에 진입했을 때, 동일한 자원에 대한 critical section에 다른 쓰레드의 접근을 막아주는 것
  • Mutex
    • 쓰레드 동기화를 구현하는 것을 뜻한다.

Mutex 변수

Mutex 생성과 초기화

  • static 생성

      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    • 생성할 경우 PTHREAD_MUTEX_INITIALIZER값으로 초기화된다.
      • 반드시 위의 값으로 초기화 해주어야 한다.
  • dynamic 초기화

      int pthread_mutex_init(
              pthread_mutex_t *mutex // mu
              pthread_mutexattr_t *attr
      )
    
    • 코드 실행중 어디서든 원하는 값으로 mutex를 초기화 할 수 있게 해주는 함수
    • return: 성공시 0, 실패시 error code

Mutex destory

int pthread_mutex_destroy(pthread_mutex_t *mutex) //제거할 뮤텍스의 포인터
  • mutex의 주소에 담긴 mutex 변수를 삭제한다.
  • return: 성공시 0 실패시 error code

Mutex lock

int pthread_mutex_lock(pthread_mutex_t *mutex)
  • mutex 변수의 상태를 확인하고 unlock 상태라면 lock 으로 바꾸어주고, 이미 lock 상태라면 unlock 상태가 될 때 까지 쓰레드가 sleep을 한다.
  • return: 성공시 0 실패시 error code
int pthread_mutex_trylock(pthread_mutex_t *mutex)
  • mutex_lock과 같으나 이미 mutex 가 lock 상태라면 sleep에 들어가지 않고 다음 으로 계속 진행한다.

Mutex unlock

int pthread_mutex_unlock(pthread_mutex_t *mutex)
  • mutex 변수를 lock 상태에서 unlock 상태로 변경해주는 함수이다.
  • return: 성공시 0 실패시 error code
  • 여러 쓰레드가 한 mutex를 두고 sleep 중일때 우르르 깨우고 경합을 시키게 되어 이때 누가 차지하는가를 두고 문제가 발생하기도 한다.

File I/O

  • open, read, write, close …

File Table

  • 커널영역에 존재하는 open file들에 대한 list이다.
  • 프로세스마다 하나씩 보유한다.
  • file discriptors 라고 하는 integer으로 접근이 가능하다.
  • File Table의 각 요소들은 파일에 대한 다음의 정보들을 가지고 있다.
    • inode
    • File offset
    • Access modes

File discriptor

  • C 언어에서는 integer type을 하고 있다.
  • 0부터 시작해 1씩 증가하며 파일들의 위치를 가르킨다.
    • 생성과 동시에 미리 할당된 인덱스들
      • 0: stdin
      • 1: stdout
      • 2: stderr
  • 일반적인 파일 이외에도 IO 디바이스, pipes, directories, socket들도 관리할 수 있다.

IO함수

파일 열기: open()

int open(const char *name, int flags)
int open(const char *name, int flags, mode_t mode) //O_CREAT 전용
  • return: 성공시 fd 반환, 오픈 실패시 -1 반환하고 errno에 적절한 값 세팅
  • name: 열고자 하는 파일 이름 (절대경로, 상대경로 모두 가능)
  • flags: 필수적인 요소 1개와 optional 한 요소 들을 으로 조합하여 만들 수 있다.
    • 필수요소
      • O_RDONLY: 읽기 전용
      • O_WRONLY: 쓰기 전용
      • O_RDWR: 읽기 쓰기 모두 가능
    • 옵션
      • O_APPEND: 파일을 열 때 off_set을 파일의 마지막으로 할당 (초깃값: 0)
      • O_CREAT: 기존 파일이 있으면 그것을 열고, 없으면 새로 생성
        • 파일접근권한도 명시해줘야 해서 mode에 그것을 명시
          • IMG_0D70E3D45E2F-1.jpeg
      • O_EXCL: 생성하되 동일한 파일이 있으면 -1반환 (exclusive)
      • O_SYNC: write 명령으로 준 데이터가 실제 파일에 쓰였는지 매번 확인
      • O_TRUNC: 기존 파일이 있다면 싹 지우고 새로 생성

파일 열기(+생성): creat()

int creat(const char *name, mode_t mode)
// open(file, O_WRONLY|O_CREAT|O_TRUNC, mode) 와 동일
  • return: 성공시 fd, 실패시 -1반환과 errno 세팅

파일 읽기: read()

ssize_t read(
        int fd,     // 읽을 파일의 fd
        void *buf,  // 읽은 내용을 담아올 버퍼
        size_t len  // 최대 얼마나 읽어올지에 대한 내용(상한)
)
  • offset이 가리키는 곳에서부터 최대 len 만큼의 바이트를 읽어와(eof만나면 거기까지) buf에 담고, fd가 가리키는 파일 테이블에 가서 읽은 길이만큼 offset을 증가시킨다. buf의 마지막에 따로 ‘\0’를 추가해주진 않는다.
  • return: 읽으려는 시도가 성공했을 경우 읽은 byte 만큼을 반환한다. offset이 EOF를 가리키는 상태라면 0을 반환한다. 읽는 도중에 다른 시그널의 개입 등으로 읽기가 중단되었을 때 읽기 성공한 바이트만큼을 반환한다. 그리고 errno를 EINTR으로 세팅한다.
    • 만약에 읽으려는 시도는 성공했으나 누군가의 방해로 0바이트를 읽었다면 0을 반환할 것이다. EOF에 도달한 경우와의 차이점은 errno == EINTR 이라는 점이다.

    읽기 실패시 -1을 반환하는데 시그널의 interrupt 인 경우일 수 있으니 -1을 받았다고 시스템 에러로 분류하는게 아니라 errno == EINTR인지 확인해 주는 것이 좋다. EINTR일 경우 read를 다시 시도한다. 다른 errno일 경우 손쓰기 어려우니 종료시키자.

  • 예제 코드

    IMG_6F680C65BADD-1.jpeg

파일 쓰기: write()

ssize_t write(
        int fd,             // 쓰기 대상 fd
        const void *buf,    // 쓸 내용을 담은 버퍼
        size_t count        // buf에서 최대 얼마나 쓸 것인지
)
  • 최대 count바이트 만큼을 buf에서 offset이 가리키는 위치에 작성한다. 매 바이트 작성하며 offset을 다음 바이트로 옮겨준다.
  • buf에 NULL문자가 있다면 NULL문자를 작성하는 것이 아니라 그 문자 전까지만 작성된다.
  • return: 성공시 작성한 바이트 크기만큼 반환하고 실패시 -1을 반환한다. 실패시 -1을 반환하고 errno를 세팅한다. read와 다르게 부분적 성공이 없고, interrupt도 일어나지 않아 -1을 반환받으면 불가능을 인정하고 에러처리하면 된다.
  • 파일에 실재로 쓰는 행위는 delay되는데 쓰는데 비용이 상당하기 때문이다. 이때문에 일정 단위로 모아서 작성하기 위해 메모리 영역의 버퍼에 그 내용들을 임시 보관한다. 즉 write()가 -1을 반환하지 않았다고 해도 실제로 쓰여졌다는 것을 보장하지는 않는다.

page cache에 저장된 delaied들을 실행하라는 독촉: fsync()

int fsync(int fd)
  • 현재 page cache에 계류중인 I/O 명령들을 실행하라는 명령이다.
    • 그러나 이 함수 또한 명확히 실행을 보장하지 않고, device driver에 요청을 보낸는 수준까지만 해준다.
    • 이게 할 수 있는 최선이다.
  • 여러번 실행하면 성능하락을 각오해야 한다.
  • return: 성공시 0 실패시 -1

파일 닫기: close()

int close (int fd)
  • 열었던 파일을 닫으라는 명령이다. 파일을 닫은 후 해당 파일을 가리키던 fd는 다시 할당가능상태가 된다. 파일을 닫았다고 page cache의 명령들이 실행되는 것이 아니니 착각하지 말자
  • return: 성공시 0 실패시 -1

File Offset

개요

  • read()와 write 함수가 동작할 파일내 위치를 저장한다.
  • 파일의 시작값에서부터 바이트 단위로 위치를 구분한다.
  • 파일이 open 될 시 (append 없을 때)0을 가리킨다.

관련 함수

  • Offset 위치 재설정: lseek()

      off_t lseek(
              int fd,
              off_t pos,  // 이동할 거리 (음수도 가능!)
              int origin  // 이동의 기준
      )
    
    • fd의 file Offset을 지정한 위치로 이동시켜준다.
    • return: 성공시 새로 설정된 offset값을 반환한다. 실패시 -1을 반환한다.
    • origin: 이동의 기준을 잡는 변수이다.
      • 종류
        • SEEK_CUR: 현위치를 기준으로 Offset을 이동시킨다.
          • 당연하겠지만 pos에 0가 온다면 이동시키지 않고 현위치를 반환한다.
        • SEEK_END: 파일의 끝을 기준으로 pos 만큼 뒤로 Offset을 위치시킨다.
        • SEEK_SET: 시작점을 기준으로 pos를 위치시킨다.
  • Offset 과 독립으로 파일을 읽는 함수: pread()

      ssize_t pread (
              int fd,
              void *buf,      // 읽은 내용 저장할 버퍼
              size_t count,   // 최대 읽을 바이트 수
              off_t pos       // 읽을 절대 위치
      )
    
    • pos 위치에서부터 최대 count 만큼의 바이트를 읽어 buf에 저장, offset은 업데이트 되지 않는다.
  • Offset 과 독립으로 파일에 쓰는 함수: pwrite()

      ssize_t pwrite (
              int fd,
              const void *buf,
              size_t count,
              0ff_t pos
      )
    
    • pos 위치에서부터 buf의 내용을 최대 count만큼 써주는 함수
  • lseek() 사용시 주의점

    • File Table은 프로세스마다 하나씩 보유하지만 File Table 의 fd는 운영체제가 갖는 글로벌 테이블에 있는 File Entry 를 가르키고(참조) 있다. 즉, open을 한 뒤 fork를 한뒤 한쪽에서 Offset를 바꿀 경우 다른 프로세스에도 그 영향이 미친다는 것이다. File Offset은 운영체제의 Global File Table에 있다고 할 수 있다.
      • 이때 글로벌 테이블은 reference count를 보유하는데 fork 전에 count가 1이었다가 fork와 동시에 2로 증가하게 되고, child나 parent process에서 파일을 닫을 경우 count가 1 줄어들고, 0이 되지 않는 이상 테이블에서 소거되지 않아 다른 프로세스에서 파일에 접근하는데 제약이 없다.

기타 참고사항(size_t)

  • size Limits
    • cpu마다 자료형 단위가 다르게 정의되어 있어 하나의 코드를 안정성있게 실행하기 위해선 size_t라는 자료형을 사용하는 것이 안전할 수 있다.
      • size_t 의 Limit : SIZE_MAX
      • ssize_t 의 Limit: SSIZE_MAX

기타 참고사항 하나를 여러번 open

  • 하나의 파일을 open 할 때마다 Global File Table에 entry가 추가 된다.
    • 즉, 이렇게 여러번 오픈하면 file offset가 중복사용되는 문제를 막을 수 있다.
    • 당연히 프로세스 파일 리스트에 할당된 fd도 다르고 close또한 각각 해주어야 한다.

Multiplexed I/O

  • select()

개요

  • File descriptor은 여러 종류가 있다.
    • 일반적인 File descriptor
    • 특별한 File descriptor
      • 모터나 센서, 키보드 등의 I/O단위가 바이트 단위인 장치
      • 다수의 바이트 Block이 입출력 단위인 I/O
      • Pipes
      • Socket
  • 어떠한 프로세스도 하나 이상의 file descriptor을 점유하는것은 불가능하다.
    • read시스템 콜이 발생하면 그것의 실행 결과 (실패든, 성공이든)를 반환받기 전까지 프로세스 자체가 block(sleep)상태로 들어간다. 즉, 하나의 fd에서 결과를 받지 않고 다른 파일에 접근하는 것은 불가능하다.
  • 여러개의 fd를 사용해야 할 때 그 목록들을 넘겨주고, 하나라도 사용 가능하면 그것을 사용하고, 모두 사용가능하지 않을 경우 잠에들지만, 하나라도 사용가능한 상태가 될 경우 깨어나는 기능이 필요

여러 File descriptor의 사용권한 질문: select()

int select(
        int n,              // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달
        fd_set *readfds,    // read 가능한지 알고싶은 fd
        fd_set *writefds,   // write 가능한지 알고싶은 fd
        fd_set *exceptfds,  // exception발생여부를 알고싶은 fd (소켓에서 사용)
        struct timeval *timeout // sleep에 들 시간의 상한 null 주면 무한정 기다림
)
// 리눅스는 timeout에서 기다린 시간만큼 차감하여 업데이트된 값을 담아줌
  • 넘겨받은 set 들 중 사용가능한 것이 있으면 사용 가능한 fd의 개수를 반환하는 함수이며 사용가능한 것이 없을 경우 사용가능한 것이 나타나거나 timeout의 만기에 도달할 때 까지 기다린다.
  • return: 정상적으로 실행될 경우 사용가능한 fd의 개수를 반환하고, timeout동안 하나도 나타나지 않을 경우0을 반환, 오류가 발생할 경우 -1을 반환하고 errno를 세팅하낟 errno == EINTER일 경우 다른 signal handler에 의해 방해받으 경우이다.
  • fd_set은 비트단위로 저장되며 어떤 fd에 대한 값을 요청하는지에 대한 정보를 저장한다.
  • fd_set의 메크로
    • FD_CLR(int fd, fd_set *set)
      • set애서 fd를 제거한다.
    • FD_ISSET(int fd, fd_set *set)
      • set에 fd가 사용가능한 상태로 표기되어 있는지 확인한다.
    • FD_SET(int fd, fd_set *set)
      • set에 fd를 추가한다.
    • FD_ZERO(fd_set *set)
      • set에 담긴 모든 내용을 소거한다.

시그널의 방해를 차단하는 select: pselect()

int pselect(
        int n,              // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달
        fd_set *readfds,    // read 가능한지 알고싶은 fd
        fd_set *writefds,   // write 가능한지 알고싶은 fd
        fd_set *exceptfds,  // exception발생여부를 알고싶은 fd (소켓에서 사용)
        const struct timespec *timeout, //sleep에 들 시간의 상한 null 주면 무한정 기다림
        const sigset_t *sigmask // 여기 담긴 시그널의 개입을 함수 실행동안 무시한다.
)

Memory Mapped I/O

  • mmap()

개요

  • 프로그램의 메모리 영역에 파일의 내용을 그대로 옮겨와서 포인터에 접근하듯이 파일내용에 directly 접근하는 것을 가능하게 해줌

파일을 메모리에 매핑하는 함수: mmap()

void *mmap (    
        void *addr,
        size_t len,
        int prot,
        int flags,
        int fd,
        off_t offset
)
  • 메모리 상에 file의 offset부터 len 바이트 크기만큼을 매핑해주는 함수
  • return: 성공시 실제로 매핑된 주소를 반환 (메모리의 시작 주소) 실패시 MAP_FAILED
  • 매개변수들
    • addr: 파일의 내용을 위치시킬 메모리 주소를 명시해준다.
      • 일반적으로 0을 넘겨 줌으로 커널이 적절한 위치에 알아서 매핑하라는 지시를 준다.
    • prot: 매핑된 메모리의 속성값을 명시해준다. chmot와 비슷…
      • 반드시 fd를 open 시 넘겨주었던 속성값의 범위 내의 값으로 설정해주어야 한다. 읽기면 읽기, 쓰기면 쓰기, 읽기 쓰기면 둘다 또는 둘중 하나
      • PROT_READ, PROT_WRITE, PROT_EXEC 를 연산자 조합하여 넘겨준다.
    • flags: 다양한 행동을 명시할 수 있음
      • MAP_FIXED: 반드시 addr로 넘겨준 위치에 매핑하고 안되면 그냥 오류를 반환하라
      • MAP_PRIVATE: 내가 매핑한 파일을 다른 프로세스가 사용하지 못하게 하라 (파일 업데이트의 즉시성이 낮아진다.)
      • MAP_SHARED: 내가 매핑한 파일은 다른프로세스와 공유하는 파일이니 파일의 변경내용의 적용 우선순위를 높여서 다른 프로세스들이 업데이트된 내용을 읽게 하라
    • offset: 파일에서 매핑될 내용의 시작위치, 이는 반드시 page-size의 배수로 할당되어야 한다.

매핑된 메모리의 해제: munmap()

int munmap (
        void *addr, // 해제를 시작할 주소
        size_t len  // 얼마만큼 해제할 것인지
)
  • addr에서부터 len만큼을 메모리에서 해제하라는 명령
  • return: 성공시 0, 실패시 -1

참고

  • mmap은 글로벌 테이블의 ref count를 증가시키고 munmap은 ref count를 감소시킨다.

  • 즉, mmap 이후 close가 있더라도 ref가 0이 되지 않아 계속 파일에 접근할 수 있다.

  • fstat(int fd, struct_stat *sb): 파일 디스크립터에 대한 정보를 sb 에 담아준다.

  • S_ISREG(sb.st_mode): sb에 담긴 파일이 regular 파일(스토리지에 들어가있는 일반적 파일)인지 검사한다.

mmap의 장점

  • low overhead: 유저영역에서 커널영역으로 커널에서 유저로 전환되는데 소모되는 오버헤드 감소
  • easy to share: 여러 프로세스들이 파일 읽고 쓰기를 메모리접근하듯, 빠른 업데이트도 되고 좋음
  • easy to seek: 포인터를 조작하여 메모리에서 이동하기 때문에 파일내 위치이동이 자유로움

mmap의 단점

  • wate of space: 몇 바이트 짜리 파일을 매핑하든 4kb(page size)가 소모됨
  • Limited mapping size: 메모리 공간을 차지하기 때문에 큰용량을 매핑할 수 없다.
  • High overhead: 외부적으로 적은 오버헤드가 소모되는 것 처럼 보일 수 있으나 내부적으로 그걸 달성하기 위해 매우 분주한다. (그럼에도 장점의 영향이 더크다.)

I/O Redirection

  • , », 2>, >&, dup

I/O operations

    • 좌항의 실행 결과로 stdout에 출력될 내용을 stdout이 아닌 우항의 파일에 덮어씌워준다.
    • 좌항의 실행 결과로 stdout에 출력될 내용을 우항의 파일에 append로 출력
  • 2>
    • 좌항의 실행 결과로 stderr에 출력될 내용을 우항의 파일에 덮어씌워준다.
  • &

    • 좌항의 실행 결과로 stderr, stdout에 출력될 내용을 우항의 파일에 덮어씌워준다.

File descriptor을 가능한 가장 낮은 fd에 하나 더 할당: dup()

int dup (int oldfd) // 재할당할 fd
  • 이미 할당이 완료된 fd에 담긴 파일을 지금 할당 가능한 새로운 fd에 배정. 기존 fd도 유지 즉 newfd = dup(1);을 실행하면 newfd엔 3이 할당되어 1과 3에 stdout이 배정된다.
  • return: 성공시 재할당된 fd, 실패시 -1
  • 예제

      close(2);
      newfd = dup(1);
    

    stderr에 출력될 내용들이 stdout으로 redirection된다.

복사하여 할당할 fd를 명시할 수 있는 dup: dup2()

int dup2(
        int oldfd, // 복사할 IO의 fd
        int newfd // 복사할 위치
)
  • oldfd를 newfd에 복사해준다. 만약 newfd가 이미 점유중이라면 기존의 file을 close하는 실행도 해준다.
    • dup2(1, 2); == close(2); dup(1);
  • return: 성공시 newfd, 실패시 -1

global file table과 dup의 관계

  • 단순히 dup()나 dup2()로 복사된 fd들은 global file table에서 같은 테이블을 가리킨다.
    • 즉, 같은 offset을 공유하고 ref count만 증가한다는 의미이다.
  • global file table을 새로 생성하는 명령은 아직까지 배운 범위 내에선 open밖에 없다.

stdio: Standard I/O Library

개요

  • Performance issues
    • Alignment

      파일은 기본적으로 block 이라는 단위로 관리된다. write 하려는 내용의 시작 주소, 크기가 block의 배수일 때 가장 좋은 속도를 낸다.

    • Number of system calls

      read write는 프로세스 영역에서 시스템영역으로 일종의 컨텍스트가 이동하는 행위이다. 한번에 묶어서 가능한 적은 횟수로 처리하는게 좋은 성능을 보여준다.

  • stdio: User-buffered I/O
    • 시스템 콜 횟수를 줄이기 위해 유저의 영역에서 IO 데이터를 머금고 있다가 한번에 명령을 내보내는 것. 라이브러리 수준(= 유저수준)의 버퍼링
    • stdio는 플렛폼 독립적인 유저수준 버퍼링 기능을 제공한다.
    • 개략도

      IMG_FBE1CC967E64-1.jpeg

  • 내부적으로 File discriptor을 FILE 이라는 타입의 포인터로 매핑하여 사용한다.

I/O 함수들

  • 파일을 열어주는 함수: fopen()

      FILE *fopen (   
              const char *path,   //열고자 하는 파일의 경로
              const char *mode    // 어떤 모드로 파일을 열 것인지
      )
    
    • mode 값들
      • r: 읽기모드로 파일을 연다.
      • r+: 읽기와 쓰기모드로 파일을 연다. 이때 파일이 있다면 내용을 유지하지만, 없다면 만들지 못하고 실패한다.
      • w: 쓰기모드로 파일을 연다.
      • w+: 읽기와 쓰기모드로 파일을 연다. 이때 이미 파일이 있다면 삭제하고 다시만들고, 없다면 새로 만든다
      • a: append 모드로 파일을 연다. offset은 파일의 끝을 가리킨다. 파일이 없다면 새로 만든다.
      • a+: 읽기와 쓰기모드로 파일을 연다. 파일이 없다면 새로 만든다. 당연히 이미 있다면 내용 유지된다.
    • return: 성공시 FILE pointer, 실패시 NULL
  • fd를 FILE *로 바꿔주는 함수: fdopen()

      FILE *fdopen (int fd, const char *mode)
    
    • 이미 열린 fd를 file descriptor으로 바꿔준다.
    • return: 성공시 File *, 실패시 NULL
    • mode: open으로 열며 명시한 mode의 범위에 포함되어야 한다.
  • FILE 에서 1글자를 읽어주는 함수: fgetc()

      int fgetc (FILE *stream)
    
    • stream에서 1글자를 읽어서 반환한다.
    • return: int형식으로 unsigned char을 변환해서 전달해주는데 그 이유는 오류발생시 오류코드를 반환해야 하기 때문이다.
  • FILE 에서 문자열을 읽어주는 함수: fgets()

      char *fgets(    
              char *str,  // 저장할 버퍼
              int size,   // 읽을 크기+1 *** size -1개를 읽고 마지막에 \0을 추가한다.
              FILE *stream
      )
    
    • stream에서 최대 size -1개의 문자를 읽어 str에 저장하고 str의 마지막에 ‘\0’을 추가하여 총 size개의 문자가 str에 들어간다.
    • *** 이때! \n을 읽더라도 큰 의미를 두지 않고 하나의 문자로 인식한다. 특별한 동작 없다.
    • return: 성공시 str을 반환, 실패시 NULL반환
  • 파일을 특정한 사이즈 단위로 읽어주는 함수: fread()

      size_t fread (  
              void *buf,      //읽은 내용을 저장할 버퍼
              size_t size,    // 읽기를 수행할 단위가 되는 구조의 크기
              size_t nr,      // 몇개의 구조를 읽을지.
              FILE *stream
      )
    
    • nr개의 size 크기 구조채를 익어서 buf에 담아준다.
    • return: 몇개의 구조를 읽었는지 반환한다.
    • 아까 언급되었던 block단위 읽기나 구조체단위 읽기할 때 쓰면 좋을 것 같다.
  • FILE 에 글자 1개를 쓰는 함수: fputc()

      int fputc(int c, FILE *stream);
    
    • 파일에 c에 담긴 글자 1바이트를 작성한다.
    • return: 성공시 c, 실패시 EOF
  • FILE 에 문자열을 입력하는 함수: fputs()

      int fputs (const char *str, FILE *stream)
    
    • str의 마지막은 반드시 ‘\0’가 위치해야 한다!
    • stream에 str의 내용을 입력하는데 ‘\0’을 만날때까지 계속 입력한다.
    • return: 성공시 양수를 반환, 실패시 EOF를 반환
  • FILE 에 특정한 사이즈 단위로 쓰는 함수: fwrite()

      size_t fwrite ( 
              void *buf,
              size_t size, // 작성할 구조의 크기
              size_t nr, // 몇개의 구조를 작성할지
              FILE *stream
      )
    
    • 마찬가지로 size단위로 buf내용을 nr번 뽑아 stream에 작성한다.
    • return: 성공시 nr, 실패시 nr이 아닌 모든값
      • fread와 다르게 write는 모 아니면 도 이기 때문에 nr을 다 쓰지 못한 상황은 오류상황이다.
  • 유저레벨에서 버퍼링 중인 것을 실행시키라는 명령: fflush()

      int fflush (FILE *stream)
    
    • 유저버퍼영역에 계류중인 내용들을 커널영역으로 보내라는 명령
    • return: 성공시 0, 실패시 EOF
      • fsync는 운영체제에 직접이야기 하는것이고 fflush는 유저영역이다.
  • FILE 의 offset을 이동시키는 함수: fseek()

      int fseek ( 
              FILE *stream,
              long offset, // 이동할 크기
              int whence // 이동의 기준
      )
    
    • whence를 기준으로 offset만큼 떨어진 거리에 stream의 오프셋을 이동시켜준다.
    • return: 성공시 0, 실패시 -1
    • whence
      • SEEK_SET: 파일의 시작, 즉 0을 기준으로 이동.
      • SEEK_CUR: 현재 위치를 기준으로 이동.
      • SEEK_END: 파일의 마지막위치를 기준으로 이동
  • FILE 의 offset 위치를 얻어주는 함수: ftell()

      long ftell (FILE *stream)
    
    • stream의 오프셋의 현재 위치를 반환한다.
    • return: 성공시 offset의 현재 위치를 반환, 실패시 -1을 반환.
  • fclose(FILE *stream) : 파일 포인터를 반환하는 함수
  • fcloseall(void): stdin, stdout, stderr을 포함한 모든 파일 포인터를 close

Time

시간의 종류

  • Wall time
    • 이벤트의 시간이나 유저와 소통에 사용하는 시간이다.
    • absolute time 즉 프로세스나 cpu동작과 관계 없이 계 바깥의 시간
    • 사용자가 변경하는 것이 가능하다.
  • Monotonic time
    • 시간의 간격이 일정하고 항상 증가하기만 하는 시간. 사용자가 임의조작하는것이 불가능하다.
      • System’s uptime
    • 특정한 계 내에서 시간을 측정하는데에 유용하다
  • Process time
    • 프로세스가 CPU 자원을 소모한 시간으로 CPU자원을 할당받지 않고 있을 경우엔 흐르지 않는다.
    • 프로세스 성능을 파악하는 등의 용도로 사용한다.

시간을 측정하는 하드웨어 장치

  • Real Time Clock (RTC)
    • 현실의 시간, 즉 Wall time을 측정하는데 사용될 수 있으며
    • 하드웨어가 꺼져 있는동안에도 시간은 흘러야 한다.
    • 정밀도가 높지 않은 편이다.
  • High Precision Event Timer (HPET)
    • periodic interrups를 발생시킬 수 있는 상당히 정밀한 시계이다.
    • 하드웨어가 종료될땐 동작을 하지 않는다.
  • Time Stamp Counter (TSC)
    • 시피유 사이클이 몇번 일어났는지 계측하는 시계이다.
    • HPET보다 높은 정밀도를 갖지만, periodic interrupt를 제공하지는 않는다.

하드웨어 장치를 이용하는 소프트웨어: System Clock

  • 리눅스에서는 system clock의 최소 단위 시간(tick)을 HZ라는 단위로 표기한다.
    • default 값은 250HZ로 4ms에 해당한다.
    • 1000HZ는 1ms이다.
  • System Time
    • Unix time인 1970.1.1을 기준으로 몇 초가 지났는지에 대한 시간 단위이다.
    • Wall time에 해당한다.
  • Unix 유래 time 계측
    • timeval structure timeval

        struct timeval{
            time_t tv_sec;          // 초단위 시간
            suseconds_t tv_usec;    // 마이크로 초단위 시간
        };
      
    • 현재 시간을 가져와 주는 gettimeofday()

        int gettimeofday(   
                struct timeval *tv, // 시간을 받을 장소
                struct timezone *tz // 현재 사용하지 않음 Null주면 된다.
        )
      
      • 현재 시간을 받아서 tv에 대입해주는 함수
      • return: 실패시 -1
    • 현재 시간을 설정하는 settimeofday()

        int settimeofday(   
                const struct timeval *tv,
                const struct timezone *tz
        )
      
  • POSIX 유래 time 계측
    • POSIX Clocks
      • CLOCK_REALTIME
        • 정밀도: 4ms
        • Wall time을 얻어온다.
      • CLOCK_MONOTONIC
        • 정밀도: 4ms
        • 부팅후 부터의 시간을 얻어온다.
        • 이 시간은 유저가 재설정할 수 없다.
      • CLOCK_PROCESS_CPUTIME_ID
        • 정밀도: 1 나노초
        • 해당 프로세스가 cpu를 사용한 시간을 받아온다.
        • 프로세스 안 쓰레드들은 같은 값을 공유한다.
      • CLOCK_THREAD_CPUTIME_ID
        • 정밀도: 1 나노초
        • 호출한 쓰레드가 CPU를 사용한 시간을 받아온다.
        • 쓰레드간 각각의 값을 갖는다.
    • timespec structure timespec

        struct timespec{
            time_t tv_sec;  // 초단위 시간 보관
            long tv_nsec;   // 나노초 단위 시간 보관
        };
      
      • 나노 초 단위 시간을 보관하여 더 높은 정밀도를 표현할 수 있다.
    • POSIX Clock의 정밀도를 알려주는 clock_getres()

        int clock_getres(   
                clockid_t clock_id,     // 알고싶은 posix 시계
                struct timespec *res    // 정밀도를 저장할 공간
        )
      
      • 전달받은 시계의 정밀도를 res에 담아준다.
    • 현재 시간을 얻어주는 clock_gettime(clock_id, timespec)

        int clock_gettime(  
                clockid_t clock_id,
                struct timespec *ts
        )
      
      • clock_id가 측정하고 있는 현재 값을 ts에 저장해준다.
    • 시계를 설정하는 clock_settime(clock_id, timespec)

        int clock_settime(  
                clockid_t clock_id,
                const struct timespec *ts
        )
      
      • clock_id가 보관하고 있는 값을 ts에 보관된 값으로 변경해준다.
      • 이 명령을 실행하려면 루트권한이 있어야 한다.
      • 당연하게도 clock_monotonic은 바꿀 수 없다.

Sleep

  • sleep: 잔다. CPU 자원을 배정받지 않는다

초단위로 잠을 재우는 sleep()

unsigned int sleep(unsigned int seconds)
  • seconds만큼 재우고 시간이 흐른 뒤 깨운다.
  • return: 알맞게 잤다면 0을 반환, 덜 잤다면 덜 잔 시간을 반환

나노초 단위로 잠을 재우는 nanosleep(timespec *req, timespec *remain)

int nanosleep(  
        const struct timespec *req, // 얼마나 재울지
        struct timespec *rem        // 일찍일어났다면 얼마나 일찍인지 저장해준다.
)
  • nano초 단위로 잠을 자고 중간에 잠이 깬 경우 잔여시간을 rem에 저장해준다.
  • CLOCK_REALTIME을 사용하여 계측한다.
  • return: 잘 잤다면 0 중간에 깼다면 -1

특정 시계를 기준으로 잠을 재우는 clock_nanosleep(clock_id, flag, req, rem)

  • clock_id에 명시된 시간을 기준으로 잠을 자고 일어나게 해준다.
    • ** 중요!! clock_id에 CLOCK_PROCESS_CPUTIME_ID이나 CLOCK_THREAD_CPUTIME_ID이 올 수 없다. → 영원히 자게된다.
  • select()로도 잠을 재울 수 있다. (unix clock 사용)
  • 일정 초 뒤에 프로세스에 SIGALARM을 주는 alarm()
    • 이미 설정된 alarm이 작동 중이라면 오래된 알람 해제하고 새 알람으로 교체
    • 실행한다고 잠들지 않고 SIGALARM만 예약해주는 함수

Timers

  • alarm()보다 많은 기능을 제공한다.

Interval Timer UNIX기반

  • itimer 구조체 itimerval

      struct itimerval{
          struct timeval it_interval; // 타이머의 간격
          struct timeval it_value;    // 다음 expire의 간격 0일경우 한번만 동작
      };
    
  • itimer설정 setitimer(which, itimerval newval, itimerval oldval)

      int setitimer(  
              int which,                      // 타이머 모드
              const struct itimerval *value,  // 세팅값
              struct itimerval *ovalue        // 이미 세팅된것이 있다면 그건 여기에 보관
      )
    
    • mode
      • ITIMER_REAL

        real Time을 기준으로 SIGALARM 시그널을 보낸다. (alarm과 유사)

      • ITIMER_VIRTUAL

        프로세스가 유저 space에서 돌아간 시간을 기준으로. 만기시 SIGVTALRM을 보낸다.

      • ITIMER_PROF

        유저레벨과 커널레벨에서 돌아간 시간을 기준으로 측정, SIGPROF를 보낸다.

  • itimer에 저장된 값을 가져오기 getitimer(which, itimerval)

    • which 모드에서 동작중인 itimer의 정보를 가져오는 함수

Interval Timer POSIX기반

  • Unix기반 타이머들은 간단하지만 overrun에 대응하지 못하고, 여러개 만드는데 제한이 있다.
  • 이를 해결해주는 POSIX itimer가 있으니 걱정하지말자
  • 구조체 itimerspec

      struct itimerspec{
          struct timespec it_interval;
          struct timespec it_value;
      };
    
  • Itimer 생성 timer_create(clockid, * sigevent, * timerid)

      int timer_create(   
              clockid_t clockid,      //기반이 될 posix 시계
              struct sigevent *evp,   // expire시 어떻게 알려줄 것인가
              timer_t *timerid        // 생성된 itimer id를 전달받을 곳
      )
    
    • evp
      • SIGEV_NONE: expire시에도 아무런 동작을 취하지 않는다.
      • SIGEV_SIGNAL: expire시 evp.sigev_signo에 저장된 시그널을 발생
      • SIGEV_THREAD: evp.sigev_notify_function을 메인으로 하는 쓰레드를 생성한다.
      • SIGEV_THREAD_ID: evp.sigev_signo를 evp.sigev_notify_thread_id에 명시된 쓰레드에 보내라
      • null을 줄 경우: SIGALRM을 발생
  • Itimer arm

      int timer_settime(
          timer_t timerid,
          int flags,
          const struct itimerspec *value,
          struct itimerspec *ovalue
      )
    
  • overrun횟수를 반환 int timer_getoverrun(timer_t timerid)
  • delete timer int timer_delete(timer_t timerid)
Comment

There are currently no comments on this article
Please be the first to add one below :)

Leave a Comment

내용에 대한 의견, 블로그 UI 개선, 잡담까지 모든 종류의 댓글 환영합니다!!