기술 정리/리눅스시스템프로그래밍

리눅스 시스템 프로그래밍 - 1장, 2장

투칼론 2016. 3. 9. 00:16
반응형

* 개인적으로 스터디한 내용을 메모한 내용이다.


CHAPTER 1 핵심 개념 소개 


시스템 프로그래밍이란 커널 및 핵심 시스템 라이브러리를 직접 사용하면서 하위 레벨에서 동작하는 시스템 소프트웨어를 작성하는 기술임. 셸, 컴파일러, 디버거, 시스템 유틸리티 및 시스템 데몬은 모두 시스템 소프트웨어 임. 네트워크 서버, 웹서버, 데이타베이스 역시 시스템 소프트웨어의 종류 임


1.1 시스템 프로그래밍 
시스템 프로그래밍은 주로 커널 및 시스템 라이브러리를 사용하고, 애플리케이션 프로그램은 고급 라이브러리를 사용한다. 


시스템 프로그래밍을 이해해야 작성하는 코드가 작동하는 레벨과 관계없이 더 나은 성능을 이끌어 낼 수 있다.


리눅스 시스템프로그래밍의 기본은 시스템콜, C라이브러리, C컴파일러임.

시스템콜(system call) - 리눅스 시스템콜은 300여개. 예) read(), write(), get_narea(), set_tid_address() 등. 아키텍처별 사용할 수 있는 시스템이 다를 수 있음.

시스템 콜 호출 방법은 아키텍처마다 다른데, 예를 들어 i386에서는 인터럽트 명령어에 0ㅌ80이라는 값을 넘기면 보호된 커널영역에 들어가 소프트웨어 인터럽트 핸들러를 실행한다.


C라리브러리(libc) - 리눅스는 GNU C라이브러리인 glibc가 제공 됨


C컴파일러 - GNU C 컴파일러인 gcc를 제공. 참고로 C++ 코드 표준 컴파일러로 g++ 제공함


1.2 API와 ABI 


시스템 레벨에서 보면 호환성에 영향을 주는 2가지 내용 - API와 ABI


API(Application Programming Interface)는 소프트웨어의 소스코드 레벨에서 서로 인터페이스 하는 방식을 정의. 예) C API

ABI(Application Binary Interface)는 특정 아키텍처 간에서 동작하는 소프트웨어 간의 바이너리 인터페이스를 정의. 콜링 컨벤션, 바이트 순서, 레지스터 활용, 시스템 콜 실행, 라이브러리 링크 , 라이브러리 동작방식, 바이너리 오브젝트 등


[참조]

http://spikez.tistory.com/141

http://www.slideshare.net/ssusere4785c/abi-34537158

  

1.3 표준 

리눅스는 가장 중요하고 유력한 표준인 POSIX와 SUS(Single UNIX Specification) 호환을 지키려 노력함

POSIX - 자유소프트웨어재단(FSF)의 창립자인 리차드 스톨만은 POSIX를 제안하였고, IEEE의 노력의 결과로 1988년 IEEE Std 1003.1-1998 (POSIX 1988) 표준이 발표됨.

SUS - 공개소프트웨어재단(OSF)와 X/Open을 합쳐 오픈 그룹이라는 컨소시엄을 구성하고, 오픈그룹은 SUS를 발표함. 오늘날 SUS는 최신 POSIX 표준을 포함하고, SUSv4임


C언어 표준은 1983년 ANSI 위원회를 구성하고, 표준화 작업을 1989년에 마무리하여 ANSI C(C89)를 만듬. ANSI C에 추가하여 ISO는 표준을 만들었는데, 최신 C표준은 ISO C11임


리눅스 배포판 중에서 LSB(Linux Standard Base)는 리눅스 시스템 대부분을 표준화 함.


1.4 리눅스 프로그래밍의 개념 

파일과 파일시스템 - 가장 기본적이고 핵심이 되는 추상화 개념. fd라는 기술자를 가지고 있음

일반 파일 - 연속적으로 나열된 바이트 배열에 저장된 데이터를 의미. 파일 오프셋의 최대값은 64비트. 하나의 파일은 다른 프로세스 또는 동일한 프로세스에서 한번 이상 열 수 있음

파일은 파일시스템 내에서 inode라는 고유 정수 값으로 참조되고, inode는 변견된 날짜, 소유자, 타입, 길이, 데이터 저장 위치 같은 파일에 관련된 메타데이터를 저장하고 있지만, 파일이름은 저장하지 않음.

디렉토리와 링크 - 개념적으로 디렉토리는 일반 파일과 유사하지만 이름과 inode의 맵핑만 저장한다는 점에서 차이. 링크는 이름과 inode의 쌍임.

하드링크 - 동일한 inode에 대한 여러가지 이름을 매핑함

심볼릭 링크 - 독자적인 inode를 가지고 있음. 하드링크보다 처리하는데 있어 오버헤드가 있음

특수파일 - 파일로 표현되는 커널 객체. 리눅스는 네종류의 특수 파일을 지원 (블록디바이스 파일, 캐릭터 디바이스 파일, 네임드 파이프, 유닉스 도메인 소켓)

파일시스템과 네임스페이스 - 파일과 디렉토리를 나타내기 위한 통합된 전역 네임스페이스를 제공. 전역 네임스페이스 안에 개별적으로 추가/제거를 마운트/언마운트라고 함.


프로세스 - 일반적인 실행 파일 포맷은 ELF(Extensible and Linkable Format) 임. 텍스트 섹션, 데이터 섹션, bss 섹션 등이 있음. 각 프로세스에 선형 주소 공간 제공. 가상 메모리와 페이징 기법 사용

스레드 - 프로세스 내부에서 실행하는 활동 단위이며, 코드를 실행하고 프로세스 동작 상태를 유지하는 추상적인 개념.

프로세스의 계층구조 - 첫번째 프로세스의 pid는 1이고, 프로세스 트리는 엄격한 계층 구조를 형성함.


사용자와 그룹 - 실제 uid 이외에 프로세스마다 유효 uid, 저장되 uid, 파일시스템 uid가 있음

권한 - 소유자, 그룹, 그외 사용자 3그룹에 대한 3비트씩 총 9비트로 표현됨


시그널 - 비동기식 단방향 알림 메커니즘. 대략 30개 정도의 시그널. 

프로세스 간 통신 - IPC메커니즘은 파이프, 네임드 파이프, 세마포어, 메시지 큐, 공유 메모리, 퓨텍스가 있음

헤더파일 - 표준 C계열과 일반적인 유닉스(예, <unistd.h>) 계열을 포함

에러처리 - errno 변수에 상세 에러에 대한 정보를 저장. <errno.h> 헤더파일에 정의됨


[참조]

http://blog.naver.com/kdi0373/220516882633 (하드링크 vs 심볼릭 링크)

http://goodtogreate.tistory.com/entry (프로세스 메모리 관련)

http://idkwim.tistory.com/78 (uid, 유효 uid 관련)

 

1.5 시스템 프로그래밍 시작하기 
파일 입출력은 단순한 파일 처리를 넘어서 다양한 작업과 밀접하게 관련 있음. 중요 함 



CHAPTER 2 파일 입출력 

파일 입출력은 매우 중요한 부분임. 기본적으로 파일디스크립터(fd)는 1,024이지만, 1,048,576까지 설정할 수 있음. 파일 디스크립터 0(stdin), 1(stdout), 2(stderr)은 기본적으로 열어둠.

 

2.1 파일 열기 

open() 함수.

open() 플래그는 O_RDONLY(읽기전용), O_WRONLRY(쓰기전용), O_RDWR(읽기와 쓰기) 중 하나를 포함해야 함.

그 외 플래그는 O_APPEND, O_ASYNC, O_SYNC, O_NONBLOCK, O_TRUNC 등이 설정될 수 있음

새로운 파일의 소유자 - 파일을 생성한 프로세스 euid(유효 uid) 임. 파일 그룹은 시스템 V 동작 방식은 프로세스의 egid로 설정되지만, BSD는 독자적인 방식으로 설정함에 유의.

새로운 파일의 권한 - open() 시 O_CRETE 플래그 사용한 경우에는 꼭 mode를 확인할 것. 

creat() 함수 - O_WRONLY | O_CREAT | O_TRUNC 조합의 시스템 콜

반환값과 에러코드 - 성공하면 fd 반환, 에러가 발생하면 -1을 반환하고, errno에 적절한 에러값에 대해 설정


2.2 read()로 읽기 
ssize_t read (int fd, vodi *buf, size_t len);

fd가 참조하는 파일의 현재 파일 오프셋에서 len 바이트만큼 buf로 읽어 들인다. 

반환값 - len보다 작은 양수값을 반환하는 경우가 있음 - 시스널이 시스템콜을 중단시키거나 파이프가 깨지는 등. 파일끝(EOF) 인경우는 0을 반환. 

read() 호출에 대해 다양한 가능성을 주의해야 함

전체 바이트 읽기, 논블록 읽기, read() 크기 제약 등에 대해 이해해야 함


2.3 write()로 쓰기 
ssize_t write (int fd, const void *buf, size_t count);

count 바이트만크 fd에 buf 내용을 기록함

read() 처럼 부분 쓰기를 일으킬 가능성 있음

O_APPEND 옵션을 이용해서 fd를 덧붙이기 모드 활용 

O_NONBLOCK 옵션, 에러코드 등 이해해야 함

write는 사용자 영역에서 커널에 넘긴 버퍼에서 커널 버퍼로 데이터를 복사된 상태이긴 하지만 의도한 목적지에 데이터를 썼다는 보장은 못함

최대 버퍼 나이 값은 /proc/sys/vm/dirty_expire_centiseconds에서 센티 초(1/100초) 단위로 설정 가능


[기타1] read()/write() 함수 사용 시에 부분읽기/쓰기를 피하기 위해서 별도 함수로 작성하여 실제 많이 사용

[기타2] 소켓이 full난 경우 write()시 부분 쓰기일수도 있음

[기타3] EPIPE가 자주남 - SIGPIPE 시그널 처리 필요


2.4 동기식 입출력 


  • fsync(fd) : write()는 버퍼에 기록하고 리턴하지만, fsync()는 디스크에 기록하고 결과를 리턴함
  • fdatasync(fd) : fsync()는 데이터와 inode 갱신 모두 완료 후에 리턴하지만, fdatasync()는 데이터 갱신 완료 후에 리턴, fsync()보다 빠를 수 있음
  • sync(void) : 모든 fd에 대한 데이터와 inode 갱신 한 후 결과 리턴
  • 파일 오픈 시에 O_SYNC(동기화), O_DSYNC(데이터만 동기화), O_RSYNC(쓰기/읽기 동기화) 플래그 설정으로 동기식 입출력 가능

[기타1] fflush와 fsync 차이점


[참조]

http://www.joinc.co.kr/modules/moniwiki/wiki.php/man/2/lseek 


2.5 직접 입출력 
리눅스커널은 디바이스와 애플리케이션 사이에 캐시, 버퍼링, 입출력관리 같은 복잡한 계층을 구현함. O_DIRECT 플래그를 사용하면 커널이 입출력 관리를 최소화. 페이지 캐시를 우회하여 사용자 영역 버퍼에서 직접 디바이스로 입출력 작업 시작


데이타베이스와 같은 동기화가 중요한 솔루션은 직접 캐시를 구현하고, O_DIRECT 옵션을 이용함


2.6 파일 닫기
파일을 닫더라도 파일을 디스크에 강제로 쓰지 않는다.

close()의 반환값을 반드시 체크하여 오류 처리할 것


2.7 lseek()로 탐색하기


  • lseek(fd, offset, whence) : 파일 읽기, 쓰기 위치를 재지정 또는 변경함
  • whence : SEEK_SET(파일 처음 기준), SEEK_CUR(파일 현재위치), SEEK_END(파일 끝 기준)
  • 참고로, fseek(fp, ..) 파일 포인터 인자임
  • lseek() 은 스레드 safe하지 않음


[참조]

http://www.joinc.co.kr/modules/moniwiki/wiki.php/man/2/lseek

http://forum.falinux.com/zbxe/index.php?document_srl=519546&mid=C_LIB

 
2.8 지정한 위치 읽고 쓰기

pread(), pwrite()함수를 통해 읽고 쓸때, offset을 지정할 수 있음


2.9 파일 잘라내기 
ftruncate() - 파일에서 len 크기만큼 잘라낸다

truncate - 파일 경로에서 len 크기만큼 잘라낸다


2.10 다중 입출력 

  • 리눅스는 select, poll, epoll 인터페이스라는 세 가지 다중 입출력 방식을 제공


  • select() 함수 : 파일 디스크립터가 입출력을 수행할 준비가 되거나 옵션으로 정해진 시간이 경과할 때까지만 블록된다.

 int select (int n,

    fd_set *readfds,

    fd_set *writefds,

    fd_set *exceptfds,

   struct timeval *timeout);


 FD_CLR(int fd, fd_set *set);          // set에서 fd를 제거

 FD_IS_SET(int fd, fd_set *set);      // set에 fd가 존재하는지 검사

 FD_SET(int fd, fd_set *set);          // set에 fd를 추가

 FD_ZERO(fd_set *set);               // set에서 모든 fd 제거, select 호출 전에 사용해야 함

    

select()는 4.2BSD에서 소개되어 큰 인기를 얻었지만, POSIX에서도 자체적인 해법 pselect()를 지원

pselect 시스템을 추가한 이유는 fd와 시그널을 기다리는 사이에 발생할 수 있는 경쟁상태(race condition)을 해결하기 위한 sigmask 인자를 추가함


[참조]

http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/Network_Programing/Documents/select

 

[기타] select()로 read처리 시에는 받은 메시지를 별도 메모리 버퍼에 저장하여 모든 데이타를 받았는지 별도 관리가 필요함


poll() 함수

int poll(struct pollfd *fds, nfds_t nfds, int timeout);


poll()과 select() 비교


epoll()은 poll()이나 select() 보다 훨씬 뛰어난 리눅스의 입출력 멀티플렉싱 인터페이스를 제공


2.11 커널 들여다보기


가상파일시스템(VFS), 페이지 캐시, 페이지 쓰기 저장


  • 가상파일시스템 : 모든 파일 시스템은 동일한 개념과 동일한 인터페이스, 동일한 호출을 지원함
  • 페이지 캐시 : 디스크 파일 시스템에서 최근에 접근한 데이터를 저장하는 메모리 저장소. 커널이 파일 시스템 데이터를 탐색하는 첫번째 장소. 스왑과 캐시의 균형은 /proc/sys/vm/swappness를 통해 설정. 
  • 페이지 쓰기저장 : 버퍼를 통해 쓰기 작업을 지연시킴. 쓰기저장은 여유메모리가 설정된 경계 값 이하 또는 설정값보다 오래동안 유지된 버퍼