read() 시스템 콜 직접 사용해보기 실습

존재하는 파일로부터 데이터를 읽기

 

 

 

사용한 터미널 명령어 전체보기

vi file_test.c

먼저 vi 명령어로 file_test.c 파일을 생성해준다.

 

 

 

file_test.c 파일에 대한 vi 편집기 내부에서는 다음과 같이 작성했다.

/* file_test1.c: read data from a file, by mjson. jinn_o@naver.com */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errono.h>
#define MAX_BUF 16
char fname[]="alphabet.txt";

int main()
{
    int fd,size;
    char buf[MAX_BUF];
    
    fd=open(fname,O_RDONLY);
    if(fd<0){
        printf("Can't open %s file with errno %d\n",fname,errno);
    }
    size=read(fd,buf,MAX_BUF);
    if(size<0){
        printf("Can't read from file %s, size=%d\n",fname,size);
        exit(-1);
    }
    else
        printf("size of read data is %d\n",size);
    close(fd);
}

 

 

 

gcc -o file_test1 file_test1.c

그리고 해당 .c 파일을 컴파일 해서 실행파일을 만들어 주었다.

 

 

 

./file_test1

 파일을 처음 실행했을 때는 에러넘버 2번의 오류가 발생한다. 참고로 에러넘버 2번의 에러는 No such file or directory 의 오류이다. 즉, 실행 시에 필요한 파일이나 디렉토리가 없어서 발생하는 오류이다.

 따라서 alphabet.txt 파일을 생성하고 다시 file_test1 을 실행했더니 오류가 발생하지 않고 잘 출력된 것을 볼 수 있다.

스트림 Stream ?

 리눅스 프로그래밍에서의 스트림은 바이트의 흐름, 즉 바이트 스트림(byte stream)을 말한다. 스트림을 바이트들이 흘러다니는 길이라고 생각해도 좋다. 스트림은 파일 디스크립터로 표현되고, read()나 write()를 통해 읽고 쓸 수 있다. 예를 들면 파일을 open()하면 read()나 write()를 통해 파일의 내용을 읽거나 쓸 수 있는데, 이것이 바로 스트림인 것이다. 같은 맥락에서 파이프라인이나 소켓도 스트림이다.

 

 

 

 

 

파일에 연결된 스트림

 스트림은 리눅스의 여기저기서 사용된다. 예를 들어 프로세스가 파일의 내용을 읽고 싶다면 어떻게 해야 할까? 파일에 연결된 스트림을 만들도록 커널에(시스템 콜을 사용하여) 의뢰한다. 그리고 다시 시스템 콜을 사용하여 파일의 내용을 읽는다. 스트림에서 바이트 열을 꺼내는 것을 읽는다(read)고 한다. 반대로 바이트 열을 흘려보내는 것을 쓴다(write)고 한다.

 

 

 

 

 

디바이스에 연결된 스트림

 스트림은 이외에도 바이트 열이 흘러갈 수 있는 곳이라면 어떤 것도 스트림에 연결될 수 있다. 예를 들어 SSD나 HDD, 키보드와 같은 하드웨어도 마찬가지다. SSD나 HDD는 바이트 덩어리라고 볼 수 있는 하드웨어이므로 당연히 스트림이 연결될 수 있다. 키보드도 입력된 키를 바이트 열로 보내는 장치이므로 스트림의 끝단에 위치할 수 있다.

파일 시스템에서 살펴본 디바이스 파일은 스트림 연결에 사용된다.

 

 

 

 

 

파이프 pipe

바이트 열이 흘러다니는 곳이 스트림이다. 프로세스에 스트림이 연결되어 바이트 열이 흘러간다. 스트림의 양 끝에 프로세스가 있는 구조의 스트림을 파이프(pipe)라고 한다. 명령어의 출력을 less 명령어로 보거나, grep 명령어로 검색하거나 할 때 파이프를 사용한다.

 

 

 

 

 

네트워크 통신 network communication

스트림이란 바이트 열의 통로다. 바이트 열이 잘 수송될 수 있다면 스트림이 다른 컴퓨터로까지 연결될 수도 있다. 이것이 네트워크 통신이다. 네트워크 통신에서는 프로세스 간 스트림 연결이 많아 스트림의 끝단에 프로세스라고 표시했지만, 파일이 연결될 수도 있다.

프로세스 간 통신 InterProcess Communication, IPC

파이프나 네트워크 통신과 같이 프로세스 간에 스트림을 통해 데이터를 주고받으며 의사소통하는 것을 일반적으로 프로세스 간 통신이라 한다. 스트림은 프로세스 간 통신에 있어 중요한 역할을 수행한다. 그러나 스트림만이 유일한 프로세스 간 통신 방법인 것은 아니다. 예를 들어 POSIX IPC 는 스트림을 사용하지 않는 프로세스 간 통신 기법이다.

파일 디스크립터 File Descriptor

프로세스에서 파일을 읽거나 쓸 때 혹은 다른 프로게스와 데이터를 주고받을 때 스트림을 사용한다고 했었다. 그렇다면 우리가 만든 프로그램에서 스트림을 사용하려면 어떻게 해야 할까? 이 때 사용하는 것이 파일디스크립터이다. 커널이 스트림을 열 때 부여하는 번호를 의미한다. 커널이 만들어 준 스트림의 번호(파일 디스크립터)를 알아야 스트림을 본격적으로 사용할 수 있게 되는 것이다.

  • 프로그램에서 보조 기억 장치에 있는 파일에 접근하는 데 이용하는 여러 가지 파일 정보를 담고 있는 자료 구조. ⇒규범 표기는 미확정이다. [네이버 국어사전]
  • 컴퓨터 프로그래밍 분야에서 파일 서술자 또는 파일 기술자는 특정한 파일에 접근하기 위한 추상적인 키이다. 이 용어는 일반적으로 POSIX 운영 체제에 쓰인다. [위키백과]

 

 

파일 디스크립터를 알면 inode 를 알 수 있고, 실제 Disk 블럭으로도 갈 수 있다. 즉, 파일 디스크립터는 파일 이름을 대신한다고 이해해도 좋다. 파일을 관리하기 위한 객체인 것이다.

 

 

표준 입력, 표준 출력, 표준 에러 출력

보통 셸을 통해 프로세스가 생성되는 경우, 세 개의 스트림이 기본으로 생성되며, 이에 대한 파일 디스크립터 값이 미리 할당된다. 이 세 개의 스트림이란 표준입력(standard input), 표준 출력(standard output) 그리고 표준 에러 출력(standard error output)이다. 이들 스트림은 각각 파일 디스크립터 0번, 1번, 2번에 할당된다. 이 값에 대한 매크로도 준비되어 있는데 각각 STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO다.

 

Integer Name <unistd.h> symbolic constant <stdio.h> file stream
0 Standard input STDIN_FILENO stdin
1 Standard output STDOUT_FILENO stdout
2 Standard error STDERR_FILENO stderr

시스템 콜 system call ?

리눅스의 세 가지 개념 중 '스트림(stream)'이 있었다. 이 스트림과 관련된 것은 시스템 콜과 라이브러리 함수이다. 라이브러리보다 시스템 콜이 커널 쪽에 좀 더 가깝다. 라이브러리는 유저모드에서 동작하며 시스템 콜은 커널(운영체제)에서 동작한다. 참고로 시스템 콜은, 그야말로 리눅스 프로그램에서 핵심이다.

 

>> 함께 보기 좋은 포스팅

https://jinn-o.tistory.com/119?category=971524 

 

[리눅스 프로그래밍] 리눅스 추상화의 층구조

추상화의 층구조 응용 프로그램 application program ex. hello.c ▽ 라이브러리 library ex. printf() ▽ 시스템 콜 system call ex. sys_write() ▽ 파일 시스템 file system ex. fs_write() ▽ 디바이스 드라이..

jinn-o.tistory.com

https://jinn-o.tistory.com/120?category=971524 

 

[리눅스 프로그래밍] 리눅스의 세 가지 중요 개념

리눅스에서 중요한 세 가지 개념은 바로 파일 시스템, 프로세스, 스트림 이다. 데이터에 이름을 붙여 보관하고 관리하는 파일 시스템이 있다. 그리고 어떤 활동을 하는 주체로서 프로세스가 있

jinn-o.tistory.com

https://jinn-o.tistory.com/124

 

[리눅스 프로그래밍] 스트림 stream

스트림 Stream ?  리눅스 프로그래밍에서의 스트림은 바이트의 흐름, 즉 바이트 스트림(byte stream)을 말한다. 스트림을 바이트들이 흘러다니는 길이라고 생각해도 좋다. 스트림은 파일 디스크립터

jinn-o.tistory.com

 

리눅스(유닉스)의 입출력은 대부분 다음 네 개의 시스템 콜로 처리된다.

read 스트림에서 바이트 열을 읽는다.
write 스트림에 바이트 열을 쓴다.
open 새로운 스트림을 생성한다.
close 사용 완료한 스트림을 닫는다.

 

 

 

 

 

read(2)

스트림에서 바이트 열을 읽기 위해 사용하는 시스템 콜이다.

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t butsize);
내용 설명
read(2) 에서 2 시스템 콜을 의미한다. (man 명령어에서 2는 시스템 콜을 의미한다.)
#include <unistd.h> API 를 사용하기 위해서 해당 헤더 파일을 포함해야 한다.
즉, read(2) 를 사용하기 위해서는 unistd.h를 include 해야 한다.
ssize_t 그리고 size_t sys/types.h 에 정의된 자료형으로 정수형의 별명(alias)이다.
말하자면 int 타입 또는 log 타입이다.
ssize_t는 부호가 있는 정수형이고, size_t는 부호가 없는 정수형이다.
fd 파일 디스크립터 번호
*buf 메모리 공간
bufsize buffer 의 최대 사이즈

 

 read()는 파일 디스크립터 번호인 fd에 해당하는 스트림에서 바이트 열을 읽는 시스템 콜이다. 읽기 작업이 순조롭게 완료되면 읽어 들인 바이트 수를 반환한다. 그리고 파일의 끝에 도달한 경우에는 0을 반환하고, 중간에 에러가 발생한 경우에는 -1을 반환한다. bufsize 바이트 수보다 적은 바이트 수를 읽는 경우도 많으므로 반환값을 체크하도록 코딩해야 한다.

 그런데 C 언어 문자열(char 배열)에는 임의의 바이트 열을 저장할 수 있지만, 일반적으로 문자 열의 끝에는 '\0'을 넣는 게 관례다. API 중에도 문자열의 끝에 '\0'이 있다고 전제하는 것과 그렇지 않은 것이 있어 사용에 주의해야 한다. read(2)의 경우는 읽어 들인 데이터의 끝에 '\0'가 있다고 전제하지 않는 API다. 따라서 read(2)를 통해 읽어 들인 문자열의 끝에 '\0'이 있다고 생각하고 코드를 작성해서는 안된다. 예를 들어, printf()의 경우는 문자열의 끝에 '\0'이 들어가 있다고 전제하는 API이므로 read(2)로 읽은 문자열을 그대로 printf()로 전달해서는 안된다. 이는 보안상 취약점이 될 수도 있다.

 

>> read() 시스템 콜 직접 실습해보기

https://jinn-o.tistory.com/125

 

[LPI 실습] 존재하는 파일으로부터 데이터를 읽기

vi file_test.c 먼저 vi 명령어로 file_test.c 파일을 생성해준다. vi 편집기 내부에서는 다음과 같이 작성했다. /* file_test1.c: read data from a file, by mjson. jinn_o@naver.com */ #include #include #in..

jinn-o.tistory.com

 

 

 

 

 

write(2)

스트림에 바이트 열을 쓸 때 사용하는 시스템 콜이다.

#include <unistd.h>

ssize_t write(int fd, const *bf, size_t bufsize);

 write()는 인자로 지정한 bufsize 바이트만큼 buf의 내용을 fd로 지정한 파일 디스크립터의 스트림에 쓴다. 정상적으로 쓴 바이트 수를 반환하고 에러가 발생한 경우는 -1을 반환한다.

 

>> write() 시스템 콜 직접 실습해보기

https://jinn-o.tistory.com/126

 

[LPI 실습] write() 시스템 콜 직접 사용해보기

write() 시스템 콜 직접 사용해보기 실습 실습(1)의 확장판, 터미널에 읽은 데이터를 출력하기 >> 실습(1) https://jinn-o.tistory.com/125 [LPI 실습] 존재하는 파일으로부터 데이터를 읽기 vi file_test.c 먼저..

jinn-o.tistory.com

 

 

 

 

 

open(2)

파일을 읽고 쓰는 스트림을 만들기 위한 시스템 콜이다.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 대괄호[]는 선택 인자를 의미한다.
int open(const char *pathname, int flags, [mode_t mode]);
내용 설명
const char *pathname 절대 경로 or 상대 경로.
여기에 지정한 경로의 파일에 대한 스트림을 만들고, 그 스트림을 가리키는 파일 디스크립터를 반환한다. 이러한 과정을 흔히 파일을 연다(open)고 한다.
int flags 파일을 어떤 모드로 열 것인지 지정한다.
mode_t mode 두 번째 인자 flags에 O_CREAT를 설정했을 때만 유효한 인자다. 새로운 파일을 만들 때, 그 파일의 권한을 설정한다. (S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP ... S_IROTH, ... 등등)
반환 값 성공한다면 파일 디스크립터를 반환하고, 실패한다면 -1 을 반환한다.

 

int flags

이때 1은 man 명령어에서 실행 가능한 프로그램이나 셸 명령어를 의미한다.

flag(1)의 종류 의미
O_RDONLY 읽기 전용
O_WRONLY 쓰기 전용
O_RDWR 읽고 쓰기

 

 

만약 O_WRONLY 또는 O_RDWR 을 사용하여 쓰는 스트림을 만들 때는 아래와 같은 추가적인 옵션을 지정할 수 있다.

flag(2)의 종류 의미
O_CREAT 파일이 존재하지 않으면 새로 만든다.
O_EXCL Exclusive(배타적)라는 의미이며, O_CREAT와 함께 사용되어 이미 파일이 존재하면 에러가 된다.
O_TRUNC Truncate(잘라 없애다)라는 의미이며, O_CREAT와 함께 사용되어 이미 파일이 존재하면 파일의 크기를 0으로 만든다. 즉, 기존에 작성되어 있는 데이터를 날리고 새로 작성한다. 덮어쓰기라고 생각하면 쉽다.
O_APPEND 기존의 데이터 뒤에 작성한 내용을 추가한다.
O_NONBLOCK 만약 BLOCK의 상태라면 데이터가 들어올 때까지 프로그램이 블락킹 상태가 되는데, 이때 블락킹 상태란 프로그램이 멈춘다고 생각하면 된다. 그러나 NONBLOCK 상태라면 데이터가 있든 없든 기다리지 않고 나온다. (백그라운드 작업과 비슷하다)
O_SYNC Synchronous라는 의미이며, 작업 내용이 항상 디스크에 반영되도록 한다.

 

 

 

 

 

close(2)

사용이 끝난 스트림을 닫는 시스템 콜이다.

#include <unistd.h>

int close(int fd);

close()는 파일 디스크립터 fd에 연결된 스트림을 해제한다. 오류 없이 닫히면 0, 에러가 발생하면 -1을 반환한다. 일반적으로 close()함수를 호출하는 코드는 다음과 같다.

if (close(fd) < 0) {
	// ...
}

프로세스가 종료되면 사용하던 모든 스트림을 커널이 파기하기 때문에 close()를 하지 않아도 시스템에 이상이 생기지 않을 수도 있다. 그러나 사용이 완료된 스트림은 반드시 바로 종료해주는 것이 좋다. 프로세스가 동시에 사용할 수 있는 스트림의 개수에 제한이 있기도 하고, 스트림의 반대편에 프로세스가 close()할 때까지 기다리고 있을 수도 있다. 모든 리소스는 사용이 완료되었을 때 닫아주는 것이 바람직하다.

다중 사용자 시스템 Multi-User system ?

 우리가 리눅스를 사용할 때 가장 먼저 하는 일은 로그인(login)이다. 이 과정을 통해 사용자의 홈 디렉토리에서 셸(Bash 등)이 기동되어 허가된 파일을 읽고 쓰고 실행하는 것이 가능해진다. 로그인이 필요한 이유는 여러 명의 사용자가 동시에 사용할 수 있게 만들기 위함이다. 이런 시스템을 다중 사용자 시스템(multi-user system)이라고 한다. 다중 사용자 시스템이 아닌 운영체제도 있다. iOS나 안드로이드(Android)가 그렇다. 그렇다면 왜 여러 명의 사용자가 필요할까?

 

 

 

권한의 분할

 시스템에는 libc.so.6과 같이 중요한 파일도 있고, 임시 메모 파일처럼 시스템 관점에서는 중요하지 않은 파일도 있다. 이 두 파일은 중요도가 다르다. 따라서 시스템에 있어 중요한 파일은 보통의 사용자와 구별된 별도의 사용자가 소유하도록 하고, 해당 소유자 이외에는 수정/삭제가 불가능하도록 만드는 것이다. 이런 일을 하기 위해 사용자별 파일에 대한 권한(permission)이 적용된다. 그래서 사용자별로 읽기/쓰기/실행 가능한 파일이 다르게 되는 것이다.

 

 

 

그룹 Group

 그룹을 사용하면 권한을 더 유연하게 배정할 수 있다. 가령, DVD 드라이브를 사용하도록 허가하고 싶은 사용자가 있고 그렇지 않은 사용자가 있다고 하자. 그럴 때 허가할 사용자들을 dvd 그룹에 묶고, 그 그룹에 DVD 드라이브(디바이스 파일 /dev/sr() 등)에 대한 읽기/쓰기 권한을 부여하면 된다.

 각 사용자는 항상 최소한 한 개의 그룹에 소속되어 있다. 보통 이 최초의 그룹을 기본 그룹이라고 하는데, useradd 명령어로 사용자를 만들 때 -g 옵션을 주어 지정하는 것이 가능하다. 그리고 시스템 관리자는 하나의 사용자를 기본 그룹 이외에 여러 그룹에 한꺼번에 추가하는 것이 가능하다. 사용자를 만들 때 useradd 명령어에 -G 옵션으로 지정하거나 만들어진 사용자에 대해 vigr 명령어를 사용하여 추가할 수 있다. 이렇게 기본 그룹 이외에 추가로 가지는 그룹을 부가 그룹(supplementary groups)이라 한다.

 

 

 

권한 Permission

 권한은 다음 세 집단에 대해 서로 다른 권한 설정이 가능하다.

- 파일을 소유하는 사용자

- 파일을 소유하는 그룹에 속한 사용자

- 그 외의 사용자

 그리고 권한의 종류는 세 가지이다.

- 읽을 수 있는 권한(read, r)

- 쓸 수 있는 권한(write, w)

- 실행(execute, x)

  소유 사용자 소유 그룹 그 외
  읽기 쓰기 실행 읽기 쓰기 실행 읽기 쓰기 실행
가능 r w x r w x r w x
불가능 - - - - - - - - -

예를 들어 보자.

rw-r--r-- 의 경우, 소유자만 읽고 쓸 수 있고 나머지는 읽을 수만 있다. 일반적으로 자주 적용되는 권한 설정이다.

rwxr-xr-x 의 경우, 소유자는 읽고 쓰고 실행도 할 수 있고 나머지는 읽고 실행만 할 수 있다. 프로그램이나 디렉토리에 사용되는 패턴이다.

rw------- 의 경우, 소유자만 읽고 쓸 수 있다. SSH의 비밀 키, 본인 이외 사용자가 봐서는 안 될 파일에 적용되는 패턴이다.

권한의 8진수 표기법

 권한을 표현하는 방법에는 다른 방법도 있는데, 그 중 대표적인 것으로 8진수 표기법이 있다. r=4, w=2, x=1, -=0 으로 간주하여 합계를 구하는 것이다. 그려면 "rwx" 의 경우, 4+2+1=7, "r-x" 의 경우, 4+0+1=5 가 된다. 이 값을 세 개 나열한 것이 최종적인 표기에 해당된다. 예를 들어 rwxr-xr-x 면 755, rw-r--r-- 면 644 이다.

 이러한 표기는 비트 연산을 기반으로 한다. r 이 4 인 것은 2진 표기로 100, w가 2인 것은 2진 표기로 010, x가 1인 것은 2진 표기로 001 이기 때문이다. 이러한 표기법은 C 언어에서 그대로 활용하기 쉽다.

디렉토리 권한

디렉토리의 권한은 일반 파일과 조금 다르게 적용된다.

읽기 권한이 있을 시 ls 명령어 등으로 디렉토리의 파일 목록을 확인할 수 있다.
쓰기 권한이 있을 시 그 안에 새로운 파일을 쓰거나 삭제할 수 있다.
실행 가능 권한이 있을 시 그 안에 파일에 접근할 수 있다.

 

 

 

 

 

자격 증명 Credential

 '사용자 A가 특정 파일에 접근한다'는 것은 '사용자 A의 속성을 가진 프로세스가 접근한다'는 뜻이다. 리눅스상에서 활동하는 주체는 사람이 아니라 프로세스이기 때문이다. 그래서 사용자 A가 조작한다는 것은 사용자 A의 속성을 가진 프로세스군이 조작한다는 것과 같은 말이다.

 그런데 여기서 '사용자 A의 속성'이라는 것을 바로 자격 증명(credential)이라고 한다. 자격 증명이란, '이 프로세스는 리눅스상에서 이 사용자의 대리인으로 동작하고 있다'는 증명서다. 커널은 그 증명서를 보고 '이 프로세스는 이 사용자의 파일을 봐도 괜찮구나'라고 판단한다. 거꾸로 말하면, 증명서만 있으면 누가 생성한 프로세스라도 커널은 그 사용자의 대리인으로 인정한다는 말이기도 하다.

 이런 증명서는, 로그인(login)하는 과정에서 사용자의 증명서를 가진 프로세스가 시스템상에 생성된다. 이 최초의 프로세스가 다른 명령(프로세스)를 실행할 때 증명서를 자동으로 복사하고 전달하므로 우리가 로그인해서 생성한 프로세스는 모두 사용자의 대리인 자격을 가지고 실행되는 것이다.

 

 

 

 

 

사용자 이름과 사용자 ID

 우리는 로그인할 때 사용자 '이름'을 입력하지만, 리눅스 커널은 이 사용자 이름을 기반으로 동작하지 않는다. 대신에 사용자 이름에 대응하는 사용자 ID(숫자)를 기반으로 동작한다. 파일의 소유자도 사용자 ID로 기록되며, 프로세스의 자격 증명서도 ID를 기반으로 다뤄진다.

 'ls -l'를 하면 파일의 소유자 이름이 출력되는데, 이는 ls 명령어가 매번 ID를 이름으로 변환하는 것에 불과하다. ls 외에도 사용자 이름을 입력하거나 표시하는 프로그램들은 모두 내부적으로 사용자 ID와 사용자 이름을 변환하여 사용하고 있다.

 

 

 

 

 

사용자 데이터 베이스

 그렇다면 사용자 ID의 매핑은 어디에 기록되어 있을까? 일반적으로는 /etc/passwd에 작성되어 있다.

/etc/passwd 파일 내용 세부사항
구분자 첫 번째 항목 두 번째 항목 세 번째 항목 네 번째 항목
콜론(:) 사용자 이름 x이면 비밀번호가 /etc/shadow 파일에 저장되어 있다는 뜻 사용자 ID 속한 그룹의 ID

 /etc/passwd에는 한 줄 단위로 사용자 정보가 등록되어 있다. 각 줄은 콜론(:)으로 항목이 구분되어 있는데 첫 번째 항목은 사용자 이름이고, 두 번째 항목이 x이면 비밀번호가 /etc/shadow파일에 저장되었음을 의미한다. 그리고 세 번째 항목이 사용자 ID를 나타낸다. 즉, 위 예에서 daemon 사용자의 ID는 1, bin 사용자의 ID는 2인 것이다.

 또 네 번째 항목은 사용자가 속한 그룹의 그룹 ID다. 즉 daemon 사용자는 그룹 ID가 1인 그룹에 소속되었고, bin 사용자는 그룹 ID가 2인 그룹에 소속되어 있다.

 그러나 그룹 ID가 1이라고 하면 어떤 그룹인지 알기 어렵다. 그룹 ID와 그룹 이름의 매핑은 어디에 있을까? 해당 정보는 /etc/passwd와 비슷한 형식으로 /etc/group이라는 파일에 기록되어 있다.

/etc/group 파일 내용 세부사항
구분자 첫 번째 항목 두 번째 항목 세 번째 항목 네 번째 항목
콜론(:) 그룹 이름 - 그룹 ID 부가 그룹인 사용자

 콜론(:)을 구분자로 첫 번째 항목이 그룹 이름이고 세 번째 항목이 그룹 ID다. 따라서 그룹 ID가 1인 그룹은 daemon 그룹이다. 그리고 /etc/group의 네 번째 항목에는 이 그룹이 부가 그룹인 사용자가 기록되어 있다. 위 예에서는 syslog 와 embedded가 부가 그룹으로 adm에 속하는 사용자임을 알 수 있다. (참고로 embedded는 현재 내가 대학 강좌로 듣고 있는 시스템 프로그래밍 강좌의 교수님이 강좌를 위해 생성하신 그룹이며, sys32197330 은 내 학번이다)

 

그러나 만드는 프로그램에서  사용자 이름과 사용자 ID를 다루기 위해 직접 /etc/passwd나 /etc/group을 해석할 필요는 없다. 그리고 보안상으로도 그렇게 해서는 안된다. 대신에 /etc/passwd나 /etc/group에 엑세스하기 위한 전용 API가 있으므로 그것을 사용하면 된다.

리눅스에서 중요한 세 가지 개념은 바로

파일 시스템, 프로세스, 스트림 이다.

데이터에 이름을 붙여 보관하고 관리하는 파일 시스템이 있다. 그리고 어떤 활동을 하는 주체로서 프로세스가 있다. 마지막으로 프로세스가 파일 시스템이나 다른 프로세스와 데이터를 주고받는 수단으로서 스트림이 있다. 단순한 구조이지만 이것을 올바로 이해하는 것이 리눅스 프로그래밍에 있어서 매우 중요하다.

 

 

 

 

 

파일 시스템 File System

 먼저 파일의 종류에는 보통 일반적인 파일, 디렉토리, 심볼릭 링크, 디바이스 파일, 명명된 파이프, UNIX 도메인 소켓 등이 있다. 또 파일에는 데이터 그 자체 외에도 메타 정보가 존재한다. 메타 정보 내부에는 파일의 종류(보통 파일인지, 디렉토리인지), 권한, 크기, 마지막 수정 시간 등이 들어있다. 이러한 정보는 "ls -l" 을 통해서 살펴볼 수 있다.

파일 시스템과 마운트

 파일 시스템은 어디에 있을까? 물론, SSD나 HDD, USB 메모리처럼 물리적인 기억 장치에 존재한다. 예를 들어 SSD나 HDD의 경우, 먼저 디스크를 파티션(partition) 단위로 나눠 그 위에 파일시스템을 얹어서 마운트(mount)하면 거대한 디렉토리 트리가 생성된다. mount 명령어를 사용하면, 현재 사용하고 있는 시스템에서 어떤 파일 시스템을 사용하고 있는지 확인할 수 있다. 현재 리눅스에서 가장 일반적으로 사용되는 파일 시스템은 ext4 이다.

파티션이 하나밖에 없어서 한 줄만 나왔다. 파티션이 더 있는 경우에는 그만큼 더 출력될 것이다. /dev/sda1은 디스크의 파티션에 대응하는 디바이스 파일의 이름이다. 그것이 루트 경로(/)에 마운트되어 있고 ext4라고 하는 파일 시스템을 사용하고 있음을 알 수 있다.

 

 

 

 

 

프로세스 Process

 프로세스란, 간단히 말해 실행 중인 프로그램을 말한다. 그리고 프로그램이란 파일 형태로 존재하는 실행 가능한 파일을 말한다. 예를 들어 hello.c 를 컴파일해서 만든 hello 실행파일이 있다고 해보자. 그러면 hello 는 프로그램이다. 이것을 실행하면 그때마다 새로운 hello 프로세스가 만들어진다. 즉, 하나의 프로그램이 있으면 새로운 프로세스를 계속 만들 수 있는 것이다.

 Console에서 'ps -ef'를 실행하면 현재 시스템에서 수행 중인 프로세스 목록이 표시된다.

.

.

.

뭐가 엄청나게 많은 것을 알 수 있다.

 이 때 하나의 프로그램으로 여러 개의 프로세스를 만들 수 있다는 것은, 프로그램의 이름만으로는 프로세스를 서로 구별할 수 없음을 의미한다. 여기서 도움이 되는 것이 프로세스 ID(process ID)이다. ps 명령어의 출력 결과에서 왼쪽에서 두 번째 열에 표시되는 것이 각 프로세스의 프로세스 ID이다. 이 ID는 특정 프로세스를 지정할 수 있어 유일하다(unique)고 말할 수 있다.

시그널

 프로세스 ID가 사용되는 대표적인 예로 시그널(signal)이 있다. Ctrl+C 를 눌러 실행 중인 프로세스를 멈춘 경험이 있을 것이다. 이때 사용된 기능이 바로 시그널이다. Ctrl+C 를 누르면 커널이 해당 프로세스에 인터럽트 시그널(SIGINT)을 보내, 이를 전달받은 프로세스가 자발적으로 종료한다. 시그널은 유닉스에서 예전부터 존재했고, 리눅스 프로그래밍에서 매우 중요한 위치를 차지한다.

 

 

 

 

 

스트림 Stream

여기서의 스트림은 바이트의 흐름, 즉 바이트 스트림(byte stream)을 말한다. 스트림을 바이트들이 흘러다니는 길이라고 생각해도 좋다. 또한, 스트림은 파일이나 open file로 다뤄지기도 하고, 스트림이란 단어를 FILE 타입과 STREAMS 커널 모듈의 두 가지 의미로 사용하기도 한다. STREAMS는 스트림 기능을 위해 사용되는 커널 모듈을 말한다.

파일에 연결된 스트림

 스트림은 리눅스의 여기저기서 사용된다. 예를 들어 프로세스가 파일의 내용을 읽고 싶다면 어떻게 해야 할까? 파일에 연결된 스트림을 만들도록 커널에(시스템 콜을 사용하여) 의뢰한다. 그리고 다시 시스템 콜을 사용하여 파일의 내용을 읽는다. 스트림에서 바이트 열을 꺼내는 것을 읽는다(read)고 한다. 반대로 바이트 열을 흘려보내는 것을 쓴다(write)고 한다.

디바이스에 연결된 스트림

 스트림은 이외에도 바이트 열이 흘러갈 수 있는 곳이라면 어떤 것도 스트림에 연결될 수 있다. 예를 들어 SSD나 HDD, 키보드와 같은 하드웨어도 마찬가지다. SSD나 HDD는 바이트 덩어리라고 볼 수 있는 하드웨어이므로 당연히 스트림이 연결될 수 있다. 키보드도 입력된 키를 바이트 열로 보내는 장치이므로 스트림의 끝단에 위치할 수 있다.

파일 시스템에서 살펴본 디바이스 파일은 스트림 연결에 사용된다.

파이프 pipe

바이트 열이 흘러다니는 곳이 스트림이다. 프로세스에 스트림이 연결되어 바이트 열이 흘러간다. 스트림의 양 끝에 프로세스가 있는 구조의 스트림을 파이프(pipe)라고 한다. 명령어의 출력을 less 명령어로 보거나, grep 명령어로 검색하거나 할 때 파이프를 사용한다.

네트워크 통신 network communication

스트림이란 바이트 열의 통로다. 바이트 열이 잘 수송될 수 있다면 스트림이 다른 컴퓨터로까지 연결될 수도 있다. 이것이 네트워크 통신이다. 네트워크 통신에서는 프로세스 간 스트림 연결이 많아 스트림의 끝단에 프로세스라고 표시했지만, 파일이 연결될 수도 있다.

프로세스 간 통신 InterProcess Communication, IPC

파이프나 네트워크 통신과 같이 프로세스 간에 스트림을 통해 데이터를 주고받으며 의사소통하는 것을 일반적으로 프로세스 간 통신이라 한다. 스트림은 프로세스 간 통신에 있어 중요한 역할을 수행한다. 그러나 스트림만이 유일한 프로세스 간 통신 방법인 것은 아니다. 예를 들어 POSIX IPC 는 스트림을 사용하지 않는 프로세스 간 통신 기법이다.

추상화의 층구조

응용 프로그램 application program

ex. hello.c

라이브러리 library

ex. printf()

시스템 콜 system call

ex. sys_write()

파일 시스템 file system

ex. fs_write()

디바이스 드라이버 device driver

ex. disk_write()

디바이스 자체 device itself

ex. HW

 

 

 

 

 

라이브러리와 라이브러리 함수 (feat. libc)

 라이브러리 함수는 내부적으로 시스템 콜을 사용하기도 하고, 그렇지 않을 수도 있다. 예를 들어 printf()의 경우, 내부에서 write()라는 시스템 콜을 사용하고 strlen()의 경우는 어떤 시스템 콜도 사용하지 않는다. 또한, 예전에는 시스템 콜이었던 작업이 라이브러리 함수로 구현되는 경우도 있고, 그 반대의 경우도 있다. 그래서 라이브러리 함수와 시스템 콜을 굳이 의식적으로 구분해서 사용하지 않는 경우도 많다.

 그러나 시스템 콜과 라이브러리 함수의 차이를 아는 것은 리눅스가 돌아가는 근본 원리에 해당하므로 잘 알아둘 필요가 있다. 또한, man 페이지를 참조할 때도 도움이 된다. 예를 들어 printf라는 셸 명령어도 있고 라이브러리 함수도 존재한다. 라이브러리 함수에 대한 설명을 보고 싶으면 'man 3 printf'라고 입력해야 한다. 한편 write는 셸 명령어와 시스템 콜에 각각 존재하는데, 시스템 콜에 대한 설명을 보고 싶다면 'man 2 write'라고 입력해야 한다.

 리눅스에는 여러 라이브러리가 있는데, 그 중에서 특히 중요한 라이브러라가 C 표준 라이브러리(C standard library), 약칭 libc 이다. 우분투의 경우 /lib, CentOS는 /lib64 디렉토리에 있다. libc.so.6이 libc의 중심이 되는 파일이다.

 

 

 

 

 

시스템 콜

 리눅스의 핵심에는 커널이 있고, 이 커널에 일을 맡기기 위해 시스템 콜을 이용한다. 또한 리눅스의 세 가지 중요 개념인 파일 시스템, 프로세스, 스트림도 시스템 콜을 통해 동작한다. 즉, 리눅스를 이해하는 중심에는 시스템 콜이 있다.

 하드웨어를 직접 다루는 건 커널만이 할 수 있기 때문에 일반 프로그램은 하드웨어를 조작하기 위해 커널에 의뢰해야만 한다. 커널은 시스템에서 가장 높은 위치에 있는 통치자이기도 하지만, 이처럼 밑바닥에서 혹사당하는 프로그램이기도 한 것이다.

 일반 프로그램이 커널에 디바이스 조작 작업을 의뢰하기 위해 사용하는 것이 시스템 콜(system call)이다. 시스템 콜이란 이름에서 시스템은 커널을 말하며, 커널을 부른다(call)는 의미에서 시스템 콜이라 한다. 구체적으로는 다음과 같은 시스템 콜이 존재한다.

open, read, write, fork, exec, stat, unlink

 일반적으로 시스템 콜을 호출하는 코드는 겉으로 봤을 때는 일반 함수를 사용하는 것과 큰 차이가 없다.

 

 

 

 

 

디바이스와 디바이스 드라이버

커널이 관리하는 하드웨어의 종류에는 CPU, 메모리, HDD(Hard Disk Drive), SSD(Solid State Drive), DVD-ROM 드라이브, CD-ROM 드라이브, 크래픽 어댑터(화면을 모니터에 전달), 네트워크 어댑터, 사운드 카드, 시계(하드웨어 클록) 등이 있다. 이와 같은 물리적인 부품들을 디바이스(device)라고 한다. 커널은 위에 나열한 디바이스들을 총괄한다. 그런더 HDD에도 여러 종류가 있고 종류 별로 조작 방법이 다 다르다. 따라서 종류가 다른 HDD가 세 개 있다면, 커널에도 이에 대응한 어댑터 코드가 세 개 필요하다. 이러한 디바이스를 다루는 코드는 필요에 따라 커널에 적재할 수 있도록 디자인되어 있다. 이렇게 특정 디바이스를 조작하는 소프트웨어를 디바이스 드라이버(device driver)라고 한다.

바이트 메모리 (Byte Memory)

컴퓨터는 바이트(Byte)단위로 일을 처리한다.

(좌) 바이트 메모리 자세히 (우) 바이트 메모리 간단히

그림에 나와있는 M 은 비트메모리이다. 비트메모리가 8개가 모여서 바이트 메모리가 된다.

 

>> 비트메모리?

https://jinn-o.tistory.com/117

 

[논리회로] 비트메모리 : 메모리가 비트를 저장하는 방법

메모리에 비트는 어떻게 저장될까? 비트 메모리는 위와 같은 사진으로 이루어져있다. 크게 보면 중요한 단자는 세 가지이다. input 입력단자인 i, output 출력단자인 o, 입력 제어 단자인 s. s i a b c o

jinn-o.tistory.com

 

 

 

 

 

출력 제어기

바이트 메모리와 언뜻보면 비슷해보이지만, 엄연히 다르다. 데이터를 입력하는 바이트 메모리의 경우, 비트 메모리가 필요하다. 그리고 그 비트 메모리의 핵심 기능은, 데이터를 변경하고 유지하는 것이다. 그러나 출력 제어기의 경우 데이터를 보관할 필요가 없다. 따라서 말 그대로 데이터를 내보낼 것인지 말 것인지만 정하면 된다. 즉, 바이트 메모리는 NAND 게이트들로 이루어져 있지만, 출력 제어기는 AND 게이트들로 이루어져 있다.

(좌) 출력 제어기 자세히 (우) 출력 제어기 간단히

입력 e 가 0 이면 출력하지 않고, 1 이면 출력한다는 의미이다.

 

 

 

 

이러한 바이트 메모리와 출력 제어기가 합쳐지면?

레지스터(register)가 된다!

(좌) 레지스터 자세히 (우) 레지스터 간단히

 

 

 

 

 

이러한 레지스터들은 버스(bus) 형태로 묶여있다!

(좌) 레지스터 버스 자세히 (우) 레지스터 버스 간단히
버스 형태로 묶여있는 레지스터들

버스는 바이트 데이터가 이곳저곳 갈 수 있게 해주는 8개짜리 전선 다발이다. 한 레지스터가 s를 1로 설정하고 있을 때 다른 레지스터의 e가 1이 되어 출력을 한다면, 레지스터 간의 데이터가 전달된다고 할 수 있다. 이 때 바이트를 이동해도 출발지의 바이트 내용은 절대로 지워지지 않는다. 바이트 이동이 완료되어도 출발지 바이트의 내용이 변하거나 소거되지 않는다. 오히려 목적지 바이트에 있던 원본 바이트 패턴만 사라진다. 어딘가로 '이동' 한다기보다 '복사'된다고 보는 것이 맞다.

비트 메모리

메모리에 비트는 어떻게 저장될까?

비트 메모리는 위와 같은 사진으로 이루어져있다. 크게 보면 중요한 단자는 세 가지이다. input 입력단자인 i, output 출력단자인 o, 입력 제어 단자인 s.

s i a b c o 설명
0 0 1 0 1 0 즉, s=1 이면, o 는 i 의 값을 따라간다.
그러나 s=0 이면, o 는 항상 0이다.
1 0 1 0 1
1 0 1 1 o의 이전 값이 1이었다면 c=0, o=1.
o의 이전 값이 0이었다면 c=1, o=0.
즉, s=0 이면, o는 이전 값을 기억한다.
1

핵심은, s=1 일 때는 값을 입력하고, s=0 일 때는 값을 보존한다는 것이다.

 

 

 

비트 메모리(단순화)

 

NAND 게이트

NAND 게이트란?

 AND 게이트에 NOT 을 붙인 것이다. 따라서 이 게이트를 "입력이 모두 TRUE 일때만 꺼지는 게이트" 라고 이해하면 쉽다. 그런데 왜 이 게이트가 컴퓨터를 구성할 때 가장 기본적인 게이트로 쓰일까? 그 이유는 다음과 같다.

 먼저 NAND 게이트가 가장 만들기 쉽다. 생산을 하는데 과정이 쉽고 소자도 적어서, 값싸고 안정적인 생산이 가능하다. 그리고 그 다음 이유가 핵심인데, 바로 NAND 만으로도 잘만 연결하면 모든 부품이나 장치를 만들 수 있다는 점에 있다. 이러한 이유들로 인해서 NAND 게이트는 컴퓨터에서 가장 기본적인 게이트이다.

 

 

 

NAND 게이트로 NOT 게이트 만들기

NOT 게이트 실제 내부 구조
NOT 게이트 단순화

 

 

 

NAND 게이트로 AND 게이트 만들기

AND 게이트 실제 내부 구조
AND 게이트 단순화

 

 

 

NAND 게이트로 OR 게이트 만들기

OR 게이트 실제 내부 구조
OR 게이트 단순화

 

a b c d e
0 0 1 1 0
0 1 1 0 1
1 0 0 1 1
1 1 0 0 1

 

 

 

NAND 게이트로 XOR 게이트 만들기

XOR 게이트 실제 내부 구조
XOR 게이트 단순화

 

a b c d e f g
0 0 1 1 1 1 0
0 1 1 0 0 1 1
1 0 0 1 1 0 1
1 1 0 0 1 1 0

 

+ Recent posts