기본적으로 알고있는 함수들을 제외하고 minishell을 만들 때 필요한 함수에 대해 정리해보자.
fork
#include <unistd.h>
pid_t fork(void);
fork는 자식프로세스를 만들기 위해 사용되는 함수이다. fork에 의해 생성된 자식 프로세스는 자신만의 PID를 갖게 되며, PPID는 부모프로세스의 PID를 가지게 된다.
return
- 성공할 경우 자식프로세스 PID가 부모에게 반환, 자식에게는 0 반환
- 실패할 경우 -1 리턴, error에 따른 errno 값 설정
error
- EAGAIN
- 자식프로세스를 위한 task 구조체를 할당할 수 없을 경우, 메모리 문제와 관련
wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
주로 fork() 를 이용해서 자식 프로세스(:12)를 생성했을때 사용한다. wait() 를 쓰면 자식프로세스가 종료할때까지 해당 영역에서 부모프로세스가 sleep() 모드로 기다리게 된다. 이는 자식프로세스와 부모프로세스의 동기화를 위한 목적으로 부모프로세스의 동기화를 위한 목적으로 부모프로세스가 자식프로세스보다 먼저 종료되어서 자식프로세스가 고아 프로세스(PPID = 1)인 프로세스가 되는걸 방지하기 위한 목적이다.
만약 자식 프로세스가 종료되었다면 함수는 즉시 반환되며, 자식이 사용한 모든 시스템 자원을 해제한다.
그런데 어떤이유로 부모가 wait() 를 호출하기 전에 자식 프로세스가 종료되어버리는 경우도 있다. 이럴경우 자식프로세스는 좀비(:12) 프로세스가 되는데, wait() 함수는 즉시 리턴하도록 되어있다.
wait() 의 인자 status를 통하여 자식 프로세스의 상태를 받아올 수 있는데, 자식프로세스의 상태값은 자식프로세스의 종료값 * 256 이다.
return
- 종료된 자식프로세스 ID는
- 에러일 경우 -1
- 그렇지 않을 경우 0
waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid waitpid(pid_t pid, int *status, int options);
waitpid()는 인수로 주어진 pid 번호의 자식 프로세스가 종료되거나, 시그널 함수를 호출하는 신호가 전달될때까지 waitpid 호출한 영역에서 일시 중지 된다.
만일 pid로 지정된 자식이 waitpid 함수 호출전에, 이미 종료되었다면, 함수는 즉시 리턴하고 자식프로세스는 "좀비프로세스"로 남는다.
pid 값은 다음중 하나가 된다.
- < -1
- 프로세서 그룹 ID가 pid 의 절대값과 같은 자식 프로세스를 기다린다.
- -1
- 임의의 자식프로세스를 기다린다. 이것은 wait(2)와 동일하다.
- 0
- 프로세스 그룹 ID가 호출 프로세스의 ID와 같은 자식프로세스를 기다린다.
- > 0
- 프로세스 ID가 pid의 값과 같은 자식 프로세스를 기다린다.
- options의 값은 0이거나 다음값들과의 OR이다.
return
종료된 자식 프로세스의 ID는
- 에러일 때 -1 반환
- 만약 어떤 자식도 이용할 수 없다면 0 반환
signal
#include <signal.h>
sig_t signal(int sig, sig_t func);
int sig : 시그널 번호
sig_t func: 해당 시그널을 처리할 핸들러
SIGINT(:12)를 발생시키는 가장 간단한 방법은 Ctrl + c 를 입력하는 방법이다.
signal 종류
ctrl + c : SIGINT => 프로세스를 종료시킴
ctrl + z : SIGSTP => 프로세스를 중단시킴
ctrl + \ : SIGQUIT => core dump를 남기고 프로세스를 종료시킴
kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
프로세스(:12)에게 시그널(:12)을 보낸다.
kill(2) 시스템콜(:12)은 특정 프로세스나 프로세스 그룹에 시그널을 보내기 위해서 사용한다.
- pid가 양수이면 sig 시그널을 pid로 보낸다
- pid가 0이면 현재 프로세스가 속한 프로세스 그룹의 모든 프로세스에게 sig 시그널을 보낸다
- pid가 -1이면, 1번 프로세스를 제외한 모든 프로세스에서 sig시그널을 보낸다
- pid가 -1보다 작으면, -pid 프로세스(:12)가 포함된 모든 그룹(:12)의 프로세스(:12)에게 sig 시그널을 보낸다
- sig가 0이면 어떤 시그널(:12)도 보내지 않는다. 하지만 에러는 검사할 수 있다.
return
- 성공할 경우 0 반환
- 실패할 경우 -1 반환하고 적당한 errno(:12) 값을 설정
exit
#include <stdlib.h>
void exit(int status);
exit함수는 프로그램 종료를 수행. 프로그램이 종료되면 프로그램의 제어권은 운영체제로 반환됨. 그때 exit(1)은 에러로인한 종료, exit(0)은 정상종료를 뜻함. 또한 각각 EXIT_SUCCESS, EXIT_FAILURE로 미리 정의되어있기 때문에 사용가능. 이것은 exit뿐 아니라 return 에서도 사용 가능.
getcwd
#include<unistd.h>
char *getcwd(char *buf, size_t size);
현재 작업 디렉토리의 size만큼 길이로 buf에 복사한다.
return
- 성공 시 작업 디렉토리의 경로 리턴
- 에러 시 NULL 리턴하고 errno 설정
- getcwd() 의 buf에 NULL 을 입력하게 되면 내부적으로 동적할당하여 그 주소를 리턴한다.
chdir
#include<unistd.h>
int chdir(const char *dirname);
현재 작업 디렉토리를 변경한다.
return
- 성공 시 0 반환
- 실패 시 -1 반환
stat, lstat, fstat
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
int stat(const char *file_name, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
stat() 함수를 이용하면 파일의 상태를 읽어올 수 있다.
lstat() 함수는 심볼릭 링크 파일의 원본파일의 상태를 얻어온다는 점이 stat()함수와 차이이다.
fstat() 함수는 open()등을 통해서 만들어진 파일디스크럽터를 인자로 받아들인다는 점이 stat()함수와 차이이다.
이 함수들은 성공적으로 수행될 경우 파일의 정보를 stat구조체에 복사한다. stat 구조체는 다음과 같이 정의되어있다.
struct stat {
dev_t st_dev; /* device */
ino_t st_ino; /* inode */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device type (if inode device) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last change */
};
return
- 성공 시 0
- 실패 시 -1
execve
실행가능한 파일인 filename의 실행코드를 현재 프로세스에 적재하여 기존의 실행코드와 교체하여 새로운 기능으로 실행한ㄷ. 즉, 현재 실행되는 프로그램의 기능은 없어지고 filename 프로그램을 메모리에 loading하여 처음부터 실행한다.
유닉스와 리눅스에서는 프로세스 생성(fork(2))과 실행할 binary 교체 (exec계열함수)는 분리되어 있다.
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
사용시 signal 설정이 default로 변경
execve의 의미
exec 뒤에 붙는 글자의 의미는
- l: argv가 list로 나열된다는 의미. 그 것은 끝은 NULL
- v: argv가 vector(배열)로 parameter를 하나를 받는다는 의미. 배열의 마지막 값은 NULL
- p: 첫번째 파라미터인 명령어/실행파일이 PATH로 지정된 디렉토리에 있다면 full path 또는 상대 path로 하지 않아도 된다는 뜻.
- e : 설정할 환경변수를 parameter로 받는다는 의미.
파라미터
filename
- 교체할 실행 파일 / 명령어.
- filename은 실행가능한 binary이거나 shell이어야 합한다.
- filename은 path가 설정되어 있는 디렉토리라고 하더라도
절대path나 상대path로 정확한 위치를 지정해야 합한다.
argv
- c언어의 main(int argc, char *argv[])에서 argv와 비슷하며, main함수에는 argc가 있지만
execve에는 argc가 없으므로 main의 argv에 마지막 array 다음은 NULL이어야 있는 것과 같다.
envp
- key=value형식의 환경변수 문자열 배열리스트로 마지막은 NULL이어야 합한다.
- 만약 기 설정된 환경변수를 사용하려면 environ 전역변수를 그냥 사용한다.
dup, dup2
dup(), dup2()는 파일 지정자 oldfd에 대한 복사본을 생성한다. 성공적으로 수행될 경우 oldfd 지정자에 대한 복사본은 서로 공유되어서 사용된다. 즉 lock, 파일위치 포인터, 플래그등을 공유한다. 만약 원본 파일지정자에서 위치변경이 일어났다면, 다른 복사된 파일 지정자에도 영향을 미친다.
그러나 이 두 파일지정자간 close-on-exe 플래그는 공유되지 않는다. dup()함수를 이용해서 복사되어지는 새로운 파일 지정자는 사용되지 않는 가장 작은 파일 지정자를 이용한다. dup2()함수의 디스크립터는 파일 지정자를 지정할 수 있는데, 이전에 열린 newfd가 있다면 닫고 나서 oldfd를 newfd에 복사하면 된다.
return
복사된 새로운 파일지정자를 리턴한다. 에러가 발생하면 -1을 리턴한다.
pipe
pipe를 이용하면 2개의 파일 지시자를 생성할 수 있다. 2개가 생성되는 이유는 읽기 전용과 쓰기 전용의 파이프를 생성하기 위함이다.
이들 파이프는 주로 부모프로세스와 자식프로세스간의 통신을 위한 목적으로 사용된다.
return
성공적으로 호출되었다면 0, 실패했다면 -1을 반환한다.
https://42kchoi.tistory.com/259