# <readline/readline.h> 헤더 설치
brew install readline # readline 헤더 설치
brew info readline # 설치 확인 & 경로 확인
# makefile로 컴파일 시 아래 플래그 추가
-lreadline -Lreadline/lib -Ireadline/include
# 나같은 경우는 경로가 /Users/42id/.brew/opt/readline/lib
# 환경에 따라 다 다르게 나오는 것으로 보인다
RL_LIB = -lreadline -L${HOME}/.brew/opt/readline/lib
RL_INC = -I${HOME}/.brew/opt/readline/include
$(NAME) : $(OBJS)
$(CC) $(CFLAGS) -o $(NAME) $(OBJS) $(RL_LIB)
%.o : %c
$(CC) $(CFLAGS) -I$(INCLUDE) $(RL_INC) -c $< -o $@
⚠️ readline 설치 후
export LDFLAGS="-L/Users/42id/.brew/opt/readline/lib" export CPPFLAGS="-I/Users/42id/.brew/opt/readline/include"
두 명령을 실행해 주어야 프로그램이 새롭게 설치한 readline.h에 제대로 접근함 ⚠️
-> 하지 않는 경우 rl_replace_line() 함수를 사용하지 못 함
-> Mac에 기본적으로 깔려있는 readline.h에는 존재하지 않는 함수이기 때문
#include <readline/readline.h>
char *readline(const char *str);
free
를 해주어야 한다.NULL
을 반환한다EOF
를 만나는 경우EOF
전에 처리할 문자가 있는 경우, EOF
를 개행문자처럼 처리하여 문자열을 반환NULL
을 반환입력 받은 문자열을 저장하고 그 메모리 주소를 반환한다
EOF를 만나는 경우 NULL return
프롬프트에 문장 입력받고 문자열 저장하는 용도
반드시 free 해야 함!
char *s = readline("prompt: ");
// 프롬프트에 prompt: 가 출력되고 그 뒤로 입력된 문장이 s에 저장
#include <readline/readline.h>
int rl_on_new_line(void);
0
반환, 실패 시 -1
반환 #include <readline/readline.h>
void rl_replace_line(const char *text, int clear_undo);
rl_line_buffer
에 입력받은 내용을 text
로 대체한다.clear_undo
는 내부적으로 유지하고 있는 undo_list
를 초기화할 지 그 여부를 결정하는 값이다. 값이 0
이면 초기화하지 않고, 다른 값인 경우 초기화한다. #include <readline/readline.h>
void rl_redisplay(void);
rl_line_buffer
의 값을 프롬프트와 함께 출력한다. 이 때, 프롬프트로 readline()
에서 입력된 문자열이 사용된다.signal
을 받을 때 사용된다. #include <readline/history.h>
int add_history(const char *);
readline()
을 통해 사용자가 입력했던 문자열을 저장하는 함수이다.0
반환, 실패 시 -1
반환#include "../include/minishell.h"
int main(int ac, char **av)
{
char *cmd;
(void)av;
if (ac != 1)
return (FAILURE);
while (TRUE)
{
cmd = readline("minishell_$ ");
if (!cmd)
break;
printf("%s\n", cmd);
add_history(cmd); // 추가 시 상하 방향키로 명령어 히스토리 불러오기 가능
free(cmd);
}
return (SUCCESS);
}
pid
반환, 자식 프로세스에겐 0
반환, 실패 시 0
반환int main(void)
{
pid_t pid;
pid = fork();
if (pid > 0)
printf("Parent ID : %d\n", getpid());
else if (pid == 0)
printf("Child ID : %d\n", getpid());
return (SUCCESS);
}
Parent ID : 88613
Child ID : 88614
#include <sys/wait.h>
pid_t wait(int *statloc);
statloc
의 주소를 인자로 넣어 자식 프로세스의 상태를 받을 수 있다.pid
반환, 실패 시 -1
반환 #include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
자식 프로세스를 기다릴 때 사용하는 함수, 즉, 자식 프로세스의 종료 상태를 회수할 때 사용한다.
그러나 wait()
과 다르게 자식 프로세스가 종료될 때까지 차단되는 것을 원하지 않는 경우 옵션을 이용하여 차단을 방지할 수 있다.
함수 실행 성공 시 PID
반환, 실패 시 -1
반환
pid
1. pid == -1
: 임의의 자식 프로세스를 기다림
pid > 0
pid
와 동일한 자식 프로세스를 기다림pid < -1
pid
의 절댓값과 같은 자식 프로세스를 기다림pid == 0
waitpid()
를 호출한 프로세스의 프로세스 그룹 ID와 같은 프로세스 그룹 ID를 가진 프로세스를 기다림statloc
options
WCONTINUED
WNOHANG
WUNTRACED
0
wait()
와 동일하게 작동0
입력 시int main(void)
{
pid_t pid;
int status;
int ret;
pid = fork();
if (pid > 0)
{
printf("Parent ID : %d\n", getpid());
ret = waitpid(pid, &status, 0);
printf("Parent killed\n");
}
else if (pid == 0)
{
printf("Child ID : %d\n", getpid());
usleep(100);
printf("Child killed\n");
}
return (SUCCESS);
}
Parent ID : 29162
Child ID : 29163
Child killed
Parent killed
-> 자식 프로세스가 종료된 후 부모 프로세스가 종료
WNOHANG
입력 시int main(void)
{
pid_t pid;
int status;
int ret;
pid = fork();
if (pid > 0)
{
printf("Parent ID : %d\n", getpid());
ret = waitpid(pid, &status, WNOHANG);
printf("Parent killed\n");
}
else if (pid == 0)
{
printf("Child ID : %d\n", getpid());
usleep(100);
printf("Child killed\n");
}
return (SUCCESS);
}
Parent ID : 29326
Parent killed
Child ID : 29327
Child killed
-> usleep(100)
으로 인해 자식 프로세스가 종료되지 않아 부모 프로세스부터 종료됨
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *staticloc, int options);
wait()
와 같지만 옵션과 pid
를 지정하여 사용자가 원하는 대로 제어 가능하다.waitpid(-1, status, options)
와 동일하게 동작 #include <sys/wait.h>
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
rusage
: 자식 프로세스의 리소스 사용량에 대한 정보가 담긴다.waitpid()
와 동일하게 작동
wait3()
와wait4()
는waitpid()
가 생긴 후로 구식함수가 되었다
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
신호를 처리하는 함수
성공 시 이전 핸들러를 반환, 실패 시 SIG_ERR
를 반환
sig
1. SIGABRT
: 비정상적 종료
2. SIGFPE
: 부동 소수점 오류
3. SIGILL
: 잘못된 명령
4. SIGINT
: 터미널 인터럽트 신호(ctrl + C)
5. SIGSEGV
: 잘못된 메모리 참조
6. SIGTERM
: 종료 신호
함수 포인터
1. SIG_DFL
: 신호에 대한 기본 작업으로 처리
2. SIG_IGN
: 신호 무시
3. 함수 이름 : 지정 함수 호출
#include <signal.h>
int kill(pid_t pid, int sig);
프로세스에 시그널을 전송하는 함수
성공 시 0
, 실패 시 -1
반환
pid
pid > 0
: 지정한 프로세스 ID에만 시그널 전송pid == 0
: 함수를 호출하는 프로세스와 같은 그룹에 있는 모든 프로세스에 시그널 전송pid == -1
: 함수를 호출하는 프로세스가 전송할 수 있는 권한을 가진 모든 프로세스에 시그널 전송pid < -1
: pid
의 절댓값 프로세스 그룹에 속하는 모든 프로세스에 시그널 전송int main(void)
{
pid_t pid;
int status;
int ret;
pid = fork();
if (pid > 0)
{
printf("Parnet ID : %d\n", getpid());
ret = waitpid(pid, &status, 0);
printf("Parent killed\n");
}
else if (pid == 0)
{
printf("Child ID : %d\n", getpid());
usleep(100);
kill(pid, SIGKILL); // 부모가 먼저 죽는다
printf("Child killed\n");
}
return (SUCCESS);
}
Parnet ID : 88149
Child ID : 88150
[1] 88149 killed ./minishell
#include <unistd.h>
char *getcwd(char *buf, size_t size);
현재 작업 경로를 가져오는 함수
성공 시 경로, 실패 시 NULL
리턴. 실패 시 errno
에 상세 오류 내용이 저장된다.
buf
: 경로 저장 변수
-> buf
가 NULL
인 경우 함수 내에서 malloc
후 리턴하므로 메모리 해제가 필수적이다.
size
: 버퍼에 할당된 바이트 수
int main(void)
{
char *pwd;
pwd = NULL;
printf("%s\n", getcwd(pwd, 0));
free(pwd);
return (SUCCESS);
}
/goinfre
#include <unistd.h>
int chdir(const char *)
0
, 실패 시 -1
반환int main(void)
{
char path[100];
printf("%d\n", chdir(readline("Path to change: ")));
printf("%s\n", getcwd(path, 100));
return (SUCCESS);
}
성공:
Path to change: /goinfre/
0
/goinfre
실패:
Path to change: ~
-1
/goinfre
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
파일 정보를 읽어오는 함수
심볼릭 링크 파일인 경우
1. stat
: 구조체에 원본 정보 채우기
2. lstat
: 심볼릭 링크 파일 정보 채우기
fstat
의 경우 현재 열린 파일의 정보를 읽어 온다.
-> open()
으로 연 파일 등
성공 시 0
, 실패 시 -1
반환
path
: 파일의 절대경로
buf
: stat
구조체
#include <unistd.h>
int unlink(const char *path);
하드 링크를 생성하지 않은 파일은 disk space
를 해제하여 바로 삭제한다.
하드 링크가 있는 파일은 이름을 삭제하고 하드 링크가 참조하는 카운트를 1만큼 감소한다. 하드 링크의 카운트가 0이 되면 실제 파일의 내용이 저장되어 있는 disk space
를 해제하여 완전히 삭제한다.
open()
으로 파일을 연 경우 unlink()
를 사용하면 정보는 삭제되더라도 disk space
는 해제되지 않는다.
성공 시 0
, 실패 시 -1
반환
path
: 상대 경로
int main(void)
{
int ret;
if ((ret = unlink(readline("File to delete: "))))
printf("Deleted\n");
else
printf("Fail to delete\n");
}
File to delete: ~/goinfre/a
Deleted
외 execve(file, null, null)이 않되나요..
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
현재 실행되는 프로그램의 기능을 없애고, filename 프로그램을 처음부터 실행
성공 시 0
, 실패 시 -1
반환
함수 호출 후에 일어나는 프로세스의 변화
1. signal
설정이 default
로 변경됨
2. opendir
로 열린 directory stream
이 close
됨
-> 일반 file discriptor
는 close
되지 않음
3. atexit(3)
, on_exit(3)
으로 등록된 exit handler
가 해제됨
4. 모든 스레드가 사라짐
...
filename
- 교체할 실행 파일 or 명령어
- 실행가능한 binary거나 shell이어야 함
- 절대 경로나 상대 경로로 정확한 위치를 지정해야 함
argv
- main(int ac, char **av);
의 **av
와 유사
envp
- key=value
형식의 환경변수 문자열 배열리스트로 마지막은 NULL
이어야 함
만약 이미 설정된 환경변수를 사용하고자 하면 environ
전역변수 사용
extern char **environ;
int main(int ac, char **av)
{
char **new_av;
char cmd[] = "ls";
int idx;
new_av = (char **)malloc(sizeof(char *) * (ac + 1));
new_av[0] = cmd;
for (idx = 1; idx < ac; idx++)
new_av[idx] = av[idx];
new_av[ac] = NULL;
if (execve("/bin/ls", new_av, environ) == -1) // where ls로 위치 찾을 수 있음
{
fprintf(stderr, "error: %s\n", strerror(errno));
return 1;
}
printf("ls 명령 실행으로 출력되지 않음\n");
return 0;
}
Makefile include src README.md minishell
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
dup()
은 파일 복제 시 사용하지 않는 fd
중 가장 작은 번호가 자동으로 지정되고, dup2()
는 사용자가 원하는 fd
로 지정 가능하다. 만약 사용 중인 경우 해당 fd
의 파일을 다은 후 지정한다.fd
를 얻어야 하므로 open
하여 사용한다int main(void)
{
int fd1, fd2, fd3;
fd1 = open("test", O_RDONLY);
fd2 = dup(fd1);
fd3 = dup2(fd2, 5);
printf("fd1: %d\nfd2: %d\nfd3: %d\n", fd1, fd2, fd3);
close(fd1);
close(fd2);
close(fd3);
}
fd1: 3
fd2: 4
fd3: 5
#include <unistd.h>
int pipe(int fd[2])
파이프를 생성하여 만들어진 디스크립터를 할당한다.
디스크립터를 얻으면 부모와 자식 프로세스가 사용할 수 있다.
fork()
에 의해 복사가 되지 않는다
성공 시 0
, 실패 시 -1
반환
fd[2]
- fd[0]
: 파이프로부터 읽는 디스크립터
- fd[1]
: 파이프에 쓰는 디스크립터
fd[0]
을 닫고, 표준 출력이 fd[1]
이 가리키는 스트림을 가리키도록 변경한다.fd[1]
을 닫고, 프로세스의 표준 입력을 fd[0]
이 가리키는 스트림으로 리다이렉션한다.➜ 이렇게 자식 프로세스가 쓰는 모든 데이터를 부모 프로세스의 표준 입력을 통해 읽을 수 있다
int main(int ac, char **av)
{
int fd[2];
pid_t pid;
if (pipe(fd) == -1)
{
fprintf(stderr, "pipe error: %s\n", strerror(errno));
return (1);
}
pid = fork();
if (pid == -1)
{
fprintf(stderr, "fork error: %s\n", strerror(errno));
return (1);
}
if (pid == 0) // Child process
{
dup2(fd[1], 1); // 표준 출력을 파이프의 쓰는 쪽으로 설정
close(fd[0]); // 자식은 파이프에서 읽지 않으므로 읽는 쪽을 닫는다
int ste = execlp("ls", "ls", NULL);
// execlp: PATH에 등록된 모든 디렉토리에 있는 프로그램을 실행하므로 프로그램 이름만 입력해도 실행이 됨
// ps 중복: av[0]처럼 프로그램 전체 이름을 의미하기 때문
// 즉 두 번째 인자인 ps부터 실행한다. (ps aux를 실행한 것)
if (ste == -1)
{
fprintf(stderr, "run error: %s\n", strerror(errno));
return (1);
}
}
// 부모만 여기에 도달
dup2(fd[0], 0); // 표준 입력을 파이프의 읽는 쪽으로 리다이렉션
close(fd[1]); // 부모는 파이프에 쓰지 않으므로 쓰는 쪽을 닫는다
char line[255];
while (fgets(line, sizeof(line), stdin) != 0)
printf("%s", line);
return (0);
}
Makefile
README.md
a.out
include
src
test
test.c
pipe()
는 파이프와 2개의 디스크립터를 생성하나. #include <dirent.h>
DIR *opendir(const char *);
struct dirent *readdir(DIR *);
int closedir(DIR *);
#define __DARWIN_STRUCT_DIRENTRY { \
__uint64_t d_ino; /* file number of entry */ \
__uint64_t d_seekoff; /* seek offset (optional, used by servers) */ \
__uint16_t d_reclen; /* length of this record */ \
__uint16_t d_namlen; /* length of string in d_name */ \
__uint8_t d_type; /* file type, see below */ \
char d_name[__DARWIN_MAXPATHLEN]; /* entry name (up to MAXPATHLEN bytes) */ \
}
opendir()
DIR
포인터, 실패 시 NULL
반환readdir()
NULL
반환closedir()
0
, 실패 시 -1
반환int main(void)
{
DIR *path = NULL;
struct dirent *file;
if (!(path = opendir(readline("Dir path: "))))
printf("No such path\n");
while ((file = readdir(path)))
printf("name: %s\n", file->d_name);
closedir(path);
}
Dir path: .
name: .
name: ..
name: test
name: Makefile
name: include
name: README.md
name: a.out
name: test.c
name: src
#include <unistd.h>
int isatty(int fd);
ttyname()
을 이용하여 이와 연관된 파일 이름을 얻을 수 있다.1
, 그렇지 않은 경우 0
반환int main(void)
{
int fd;
printf("%d\n", isatty(0)); // 표준입력은 터미널에 연결되어 있으므로 1을 출력한다.
fd = open("test100", O_RDWR);
printf("%d\n", isatty(fd)); // 파일은 터미널에 연결되어 있지 않으므로 0을 출력한다.
close(fd);
fd = open("/dev/ttyS0", O_RDONLY);
if (fd < -1)
{
printf("open error\n");
exit(0);
}
printf("%d\n", isatty(fd));
close(fd);
return (0);
}
1
0
0
#include <unistd.h>
char *ttyname(int fd);
NULL
반환int main(void)
{
printf("%s\n", ttyname(0));
printf("%s\n", ttyname(5));
return (0);
}
/dev/ttys000
(null)
#include <unistd.h>
int ttyslot(void);
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
#include <stdlib.h>
char *getenv(const char *name);
int main(void)
{
printf("%s\n", getenv(readline("env: ")));
}
env: HOME
/Users/42id
#include <termios.h>
int tcsetattr(int fd, int action, const struct termios * termios_p)
int tcgetattr(int fd, struct termios *termios_p)
터미널 파일 fd
에 대한 터미널 설정하고 가져온다.
tcsetattr()
: termios
구조체 초기화
: termios_p
의 내용대로 구조체 멤버를 설정한다
tcgetattr()
: action
필드의 내용대로 설정한다
- TCSANOW
: 값을 즉시 변경
- TCSADRAIN
: 현재 출력이 완료되었을 때 값 변경
- TCSAFLUSH
: 현재 출력이 완료되었을 때 값을 변경하나, 현재 읽을 수 있으며 read 호출에서 아직 반환되지 않는 입력값을 버린다
: 프로그램이 시작하기 전의 값으로 터미널 설정값을 복구하는 것이 중요함
#include <curses.h>
#include <term.h>
int tgetent(char *bp, const char *name);
int tgetflag(char *id);
int tgetnum(char *id);
char *tgetstr(char *id, char **area);
tgetent()
: 엔트리 이름을 가져옴. 성공 시 1
, 해당 엔트리가 없는 경우 0
, terminfo
데이터베이스를 찾을 수 없는 경우 -1
반환tgetflag()
: id
의 boolean
정보를 가져옴. 실패 시 0
반환tgetnum()
: id
의 숫자 정보를 가져옴. 실패 시 -1
반환tgetstr()
: id
의 문자열 정보를 가져옴. 실패 시 NULL
반환tputs()
를 이용해 반환된 문자열을 출력한다.area
에도 저장된다. #include <curses.h>
#include <term.h>
char *tgoto(const char *cap, int col, int row);
int tputs(const char *str, int affcnt, int (*putc)(int));
tgoto()
: 매개변수를 주어진 기능으로 인스턴스화. 출력은 tputs()
로 전달된다.tputs()
: str
에 패딩 정보를 적용하고 출력한다.str
은 terminfo
문자열 변수이거나 tparm()
, tgetstr()
또는 tgoto()
의 반환값이어야 한다.affcnt
는 적용되는 줄 수로, 적용되지 않는 경우 1
로 설정한다.➜ 정보가 많이 없다..🙂🙃🙂🙃
-n
옵션(개행 삭제)-nnnnn
이어도 작동 되며 -n -n
이런 식으로 중복되어도 작동 됨echo -nnnn hi
: hi%echo -n -na hi
: -na hi%echo -nnnn1 -n hi
: -nnnn1 -n hiecho -nnnn -n hi
: hi%경로 이동 명령어
절대경로 혹은 상대경로만 사용하도록 구현
현재 경로 출력
리다이렉션 앞 뒤로 공백 없어도 작동 함 .. 주의할 것
공백 기준으로 split 한 후에 redirection 있는지 한 번 더 확인해야 할 듯
command > file
# command의 출력 결과를 file로 저장
> file
# 터미널에 입력한 내용을 file에 저장
# zsh는 터미널에 입력 받을 수 있는데 bash는 작동하지 않음, 대신 file 내용이 공백으로 업데이트됨
⚠️
ls > file
을 할 때 file이 존재하지 않는 경우 file부터 만들고ls
의 결과를 file에 저장(ls
명령 실행 결과에 file도 포함이 됨) ⚠️
command >> file
# command의 출력 결과를 file 맨 뒤에 이어쓰기
>> file
# 아무 동작하지 않음(공백을 맨 뒤에 붙이는 것이기 때문)
< file
# 아무 동작 하지 않음
<< keyword
# keyword가 나올 때까지 입력만 받고 keyword 나오면 입력받기 종료
닫히지 않은 따옴표는 다룰 필요 없음
echo "${HOME}"
echo "'${HOME}'"
/Users/42id
'/Users/42id'
echo '${HOME}'
echo '"${HOME}"'
${HOME}
"${HOME}"
^C 없앨 때 tcsetattr 함수 사용 ?!
SIGINT
새로운 줄에 새로운 프롬프트 생성
초기 설정은 minishell 종료
minishell 종료
초기 설정은 새로운 줄
bash는 logout
SIGQUIT
아무런 동작도 하지 않아야함
아무 설정도 하지 않으면 minishell quit
([1] PID quit ./minishell)
bash는 아무 동작 x
ref
unlink: https://www.it-note.kr/177
pipe: https://nomad-programmer.tistory.com/110
tty: https://powergi.tistory.com/entry/terminal-%ED%86%B5%EC%8B%A0
isatty: https://www.joinc.co.kr/w/man/3/isatty
termios: https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=y851004&logNo=20063499242