🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.
file system
은 저장장치에 file을 저장하기 위한 논리적 구조가 존재하며, directory가 mount된 file system에 쌓여있는 것을 Mount-on
이라 한다. 복수계의 device로 하나의 file system을 구축하는 것이 Linux에서는 가능하며, 이를 그림으로 나타내면 아래와 같다.
하단의 file system
을 상단의 root file system
의 하위 directory에 mount
하여 file system
을 구현한다.
PC가 부팅되어야 할 때 실행되어야 할 코드가 들어있는 block이다.
file system에 대한 정보를 담고 있으며, 각 device마다 존재한다.
- file system 내부의 total block 개수
- i-node 개수
- block의 size
- free block과 used block의 개수
디스크 file과 관련된 모든 i-node들이다.
file block에 해당한다.
file system
에서 mount
된 file system
의 super block
복사본은 빠른 접근을 위해 memory
상에서 관리한다. super block
이 담고 있는 정보는 작기 때문에 file 변화시마다 바로 접근한다.
memory에 있는 data는 바로 disk에 쓰이는 것이 아니라, buffer에 모이게 된다. Unix(Linux)에는 buffer의 data를 disk에 write하기 위해 sync
와 fsync
가 존재한다.
#include <unistd.h>
void sync(void);
int fsync(int filedes);
argument
- int filedes : 바로 disk에 반영할 file의 file descriptor
return (
fsync
)
- 성공 시에 0 return
- error 발생 시에 -1 return
sync
는 모든 main memory data를 반영하며, fsync
는 전달받은 file에 대해 sync
를 진행한다. sync
는 os와 관련되어 사용되며, unix(linux)는 주기적으로 sync
를 call하는 코드를 반복한다.
중요한 차이점은 fsync
의 경우는 해당 파일에 대해 sync
가 처리될때까지 block되지만, sync
는 바로 return이 된다는 점이다. (바로 실행은 X)
Unix(linux)는 device를 숫자로 지정해 관리하며, 이를 device number
라 한다. 이는 크게 아래와 같이 나뉘며, 사용자는 device number
로 바로 device에 접근하는 것이 아닌, unix(linux)가 device number
를 mapping한 device file
을 통해 접근하게 된다.
- major number : device의 type을 의미 (device driver)
- minor number : type 내부의 순서, 각 device를 구분
주변 장치에 해당하는 device들은 file system의 file 이름을 통해 접근하며, 이러한 device file에 대한 read, write는 system과 해당 device간의 직접적인 데이터의 전송으로 이루어진다. 이러한 device file들은 /dev
directory 안에 위치하며, 일반 file들과 같이 사용될 수 있다.
ex) $ cat fred > /dev/lp
// > : redirection
device file
은 아래와 같이 크게 2가지로 나뉜다.
Block device file
- block 단위로 접근
- SSD, HDD 등
random access
가 가능- file system은 block device로만 구성 가능
Character device file
- char 단위 접근
- terminal, printer, network 등 바로 보내는 것이 중요한 device들
- 길이는 유동적이며, random access가 가능할 수도, 불가능할 수도 있음
추가적으로 Raw device
가 존재한다. block device
인데 char device
로 사용하고 싶을 경우에 해당되며, file system
을 걷어내므로 여러 단계를 거치지 않아도 되서 I/O 속도가 증가한다는 장점이 있다.
이러한 device들과 상호작용하기 위한 2가지 table이 존재하며, block device switch table
과 character device switch table
이다. 두 table 모두 device file의 i-node
에 저장된 major number
를 사용하여 인덱싱된다.
device들과 data를 주고받는 순서는 아래와 같다.
read
와write
를 call하여 device file의i-node
에 접근i-node
내의 flag를 보고block
인지char
인지 구분하고,major, minor number
를 추출major number
로 device 확인 후, driver routine 호출,minor number
로 정확한 포트 식별
st_mode
에 저장된 bit는 사실상 총 16bit
이며, 권한을 나타내는 9bit와 execute 관련 3bit을 제외하고 상위 4bit
이 추가로 존재한다. 이는 해당 file이 무슨 파일인지를 나타낸다. 이는 아래 그림과 같다.
st_mode
에서 060000 (S_IFBLK)
은 block device
를 나타내며, 020000 (S_IFCHR)
은 character device
를 나타낸다. 또한 st_rdev
는 minor number
를 의미한다. 'ls -l
을 통해 이를 쉽게 확인할 수 있다.
main function
의 prototype은 아래와 같으며, argc
는 command line에서의 인자의 개수를 의미하며, argv
는 인자가 담긴 배열의 pointer에 해당한다.
int main(int argc, char *argv[]);
exec
함수 중 하나에 의해 c program
이 kernel에 의해 실행되면 main
이 호출되기 전에 start up routine
이 실행된다. 이는 kernel에서 command line에서의 인수와 환경변수 등을 가져온다. 이는 아래와 같다.
environment list
는 char pointer의 list이며, 이 주소는 전역 변수 환경에 포함된다. 대부분의 unix(linux) 에서는 main 함수의 3번째 인자로 제공된다.
int main(int argc, char *argv[], char *envp[]);
PATH
의 경우는 file 이름이 주어졌을 때, 해당 file을 search할 directory들의 정보가 담겨있으며, :
으로 구분하여 나열되어있다.
C program
은 위와 같이 구성되어 있으며, 이를 정리하면 아래와 같다.
- Text segment : 실행할 프로그램 명령들
- Initialized data segment : 초기화된 전역 및 정적 변수들 (static 포함)
- Uninitialized data segment (bss) : 초기화되지 않은 전역 및 정적 변수들이며 program 시작 전에 kernel에 의해 0 혹은 null pointer로 초기화됨
- Stack : local 변수를 위한 메모리 공간
- Heap : 동적 메모리 할당 관련
Text segment
와 Initialized data segment
는 Program file
에 저장된다.
배열에서 하나만 초기화를 해준 경우와, 초기화를 해주지 않고 선언만 한 경우에 있어서 차이가 존재한다. 왼쪽의 경우는 initialized
에 들어가며, 오른쪽은 uninitialized
에 포함된다. 이는 size
command를 이용하여 파악할 수 있다. text는 동일하나 data
를 보면, bss
는 file size
에 포함되지 않기 때문에 init
의 경우가 더 큰 것을 볼 수 있다. (program file
에 포함되기 때문)
process
는 현재 실행되고 있는 program
을 의미하며, program
과 process
는 다르다고 할 수 있다. process
는 아래를 포함한다.
- program code
- program 변수 내의 data 값
- 하드웨어 register
- program stack
process
는 process id(pid)
가 붙어 관리되며, shell
은 새로운 process
를 생성한다.
process
에도 부모와 자식 관계가 존재하며, directory tree와 유사한 계층 구조로 이루어진다. 가장 상위에는 단일 제어 process
인 모든 process
의 조상에 해당하는 init
이 존재한다. unix(linux)는 process
생성과 조작을 위해 fork
, exec
, wait
, exit
등 system call들을 제공한다.
가장 상위 process인 init
은 bootstrap
에서 생성된다.
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
getpid
와 getppid
는 process id
를 return하며, process id
는 유일하고 음수가 아닌 정수이다. 0
, 1
은 이미 존재하며, 이후의 번호를 사용하고, 유일하기 때문에 재사용될 수 없다. getpid
는 현 process의 pid
를 return하고, getppid
는 parent process의 pid
를 return한다.
#include <unistd.h>
pid_t fork(void);
fork
는 call한 process와 동일한 새로운 process를 생성한다. child와 parent의 return값은 서로 다르다.
return
- 성공 시에는 child process는 0을, parent process는 child process의
PID
를 return- error 발생 시 -1 return
system 전체에 대한 process 수 제한이나 개별 사용자에 대한 process 수 제한에 도달하면, 즉 process의 maximum 수에 도달하면 limit error
로 errno = EAGAIN
을 return한다.
child는 parent의 data space, heap, stack의 복사본을 받지만, text segment는 공유하게 된다. 즉, 코드는 복사하지 않고 parent의 코드를 공유한다. 또한 parent와 child 둘 중 어떤 process가 먼저 실행될지 알 수 없다.
이러한 과정을 그림으로 나타내면 아래와 같다.
0
은 이미 할당되어 있으므로 사용될 일이 없기 때문에 child를 나타낼때 사용한다. 이에 대한 예시 코드는 아래와 같다.
#include <unistd.h>
int main() {
pid_t pid;
printf(“Just one process so far\n”);
printf(“Calling fork …\n”);
pid = fork();
if (pid == 0)
printf(“I’m the child\n”);
else if (pid > 0)
printf(“I’m the parent, child has pid %d\n”, pid);
else
printf(“Fork returned error code, no child\n”);
return 0;
}
위의 코드와 같이 fork
를 사용할 경우에는 child
에서 실행될 코드와 parent
에서 실행될 코드가 둘 다 포함되어야 하므로 비효율적이다. 따라서 exec
을 사용한다.
exec
는 새로운 program의 실행을 시작한다. execl
, execv
, execle
, execve
, execlp
, execvp
로 총 6가지이며, 새로운 process가 생성되지는 않기 때문에 PID
는 변경되지 않는다. 단순히 현 process를 disk의 새로운 program으로 대체하는 것이며, execve
가 kernel 내 systam call에 해당한다. (즉, 다른 형태는 변환을 통해 execve
로 실행됨)
# include <unistd.h>
int execl(const char *pathname, const char *arg0, …, NULL);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, …, NULL, char *const envp[]);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, …, NULL);
int execvp(const char *filename, char *const argv[]);
전체적인 형태는 위와 같으며, exec
뒤에 붙는 문자에 따라 의미가 달라진다.
l : 인자를 list 형태로 줌
v : 인자를 vector 형태로 줌
e : 환경변수를 포함
p : PATH 환경변수를 바탕으로
argument
- const char *pathname : 절대, 상대 경로로 표현되는 file
- char *const argv[] : vector로 전달되는 인자
- char *const envp[] : 환경변수
- const char *filename : 경로 표시 제외한 file 이름
return
- 정상 수행 시 return 없음
- error 발생 시 -1 return
exec
는 새로운 하위 process를 생성하지 않으며, 실제 program이나 실행 권한이 있는 shell script를 포함해야 한다.
stuct stat
에서 st_dev
는 disk drive에 대한 정보에 해당하며, 실제 장치 file 정보에 해당하는 것은 st_rdev
이다.
#include <sys/sysmacros.h>
unsigned int major(dev_t dev);
unsigned int minor(dev_t dev);
major
과 minor
은 각각 device의 major number
와 minor number
를 return한다.
st_mode
와 아래 bit들이 동일한지를 판별하면, 해당 file이 어떤 type인지 판별할 수 있다.
st_mode == 0010000 : FIFO (p)
st_mode == 0020000 : character dev (c)
st_mode == 0040000 : directory (d)
st_mode == 0060000 : block dev (b)
st_mode == 0100000 : regular (-)
st_mode == 0120000 : symbolic link (l)
-> `0170000`과 `&`연산을 통해 판별이 가능하다.
program 실행 시에 뒤에 &
을 붙여주면, background 수행을 하며, process 종료 전에 shell로 복귀하게 된다.
현재 수행중인 process 정보를 출력하며, option은 아래와 같다.
-e
: system process 전체를 전부 보여줌-f
: full format으로 보여줌
위 둘에 대한 예시는 아래와 같다.
execlp
와 execvp
에서 사용하는 PATH 환경변수는 echo $PATH
로 확인이 가능하며, export PATH=$PATH:추가하고싶은 경로
로 추가가 가능하다. 이에 대한 예시는 아래와 같다.
하지만 이는 해당 shell에서만 적용되므로, 종료하면 기본 PATH로 돌아간다. 따라서 모든 shell에서 PATH를 변경하고 싶다면 ~/.bashrc
에 PATH=$PATH:추가하고싶은 경로
를 추가하면 된다.
특정 경로를 PATH에 추가해주면, 해당 경로에서는 경로명이 아닌 file명으로 program 실행이 가능하다.
which 명령어
로 특정 명령어의 위치(path)를 return해준다. whereis
는 명령어의 바이너리(실행파일), 소스, 매뉴얼 파일의 위치를 출력한다.