멀티 프로세스
와 리눅스 bash
의 내부 동작원리와 팀 프로젝트에 대해 배웁니다.
30개가 넘는 c 파일이어서 이번에는 세세하게 설명하지 않습니다.
Built_in
명령어 아래의 명령어들을 직접 구현합니다.
자료구조는 리스트(list)
와 동적 배열(vector)
를 만들어 사용했습니다.
NAME = minishell
CFLAGS = -Wall -Wextra -Werror
CMPFLAGS = libft/libft.a -lreadline -L${HOME}/.brew/opt/readline/lib
INFLAGS = -I${HOME}/.brew/opt/readline/include
BUILTIN_SRCS = $(addprefix builtin/, builtin_util.c cd.c echo.c env_split.c env.c exit.c export.c export2.c pwd.c unset.c)
ETC_SRCS = $(addprefix etc/, signal.c signal2.c vector.c vector2.c)
EXEC_SRCS = $(addprefix exec/, run_builtin.c run_child_checkaccess.c run_child_errcheck.c run_child.c run_command.c run_single_builtin.c run_split.c run_split_sep.c run_util_general.c run_util_quote.c)
MAIN_SRCS = $(addprefix main/, main.c main_heredoc1.c main_heredoc2.c main_while_init.c)
PARSE_SRCS = $(addprefix parsing/, parse.c parse_env_expansion.c parse_env_expansion2.c parse_qm_expansion.c parse_sep.c parse_tokenize.c parse_tokenize2.c parse_util.c parse_util2.c)
HEADER = main.h
SRCS := $(BUILTIN_SRCS) $(ETC_SRCS) $(EXEC_SRCS) $(MAIN_SRCS) $(PARSE_SRCS)
OBJS = $(SRCS:.c=.o)
all : $(NAME)
$(NAME) : $(OBJS) $(HEADER) libft/libft.a
cc $(CFLAGS) $(OBJS) -o $(NAME) $(CMPFLAGS)
libft/libft.a :
make -C libft bonus
%.o : %.c $(HEADER)
cc $(CFLAGS) -c $< -o $@ $(INFLAGS)
clean :
make -C libft clean
rm -f $(OBJS)
fclean :
make clean
make -C libft fclean
rm -f $(NAME)
re :
make fclean
make all
.PHONY : all clean fclean re
int main(int argc, char **argv, char **envp)
{
t_list *list;
t_copy env;
char *line;
int result;
result = 0;
line = 0;
list = NULL;
main_init_env(&env, envp, argv, argc); // 환경변수 복사본 만들기
while (1)
{
if (main_while_init(&list, &line, &result))
{
free(line);
continue ;
}
if (!(main_single_builtin_check(&list, &result, &env)))
result = command_run(list->next, &env, result);
delete_local_file(list->next);
free(line);
free_list(list);
}
}
매개변수로 받은 환경 변수를 export
와 env
로 활용하기 위해 복사본을 2개 만들어줍니다.
그리고 while(1)로 계속 프롬프트를 띄워주는 방식으로 진행합니다.
int main_while_init(t_list **list, char **line, int *result)
{
handle_signal(); // 시그널 입력받기
g_result = 0;
(*line) = reading(); // 프롬프트 띄우고 라인 입력받기
if (g_result)
(*result) = g_result; // 시그널 들어오면 errno값 변경
if ((*line) == 0 || (**line) == 0)
{
free(*line);
(*line) = 0;
return (1);
}
(*list) = first_parsing(line); // 입력값 line을 parsing해서 토큰단위로 list에 담기
if ((*list) == 0)
return (1);
if (command_check((*list))) // list에 담긴 토큰이 명령어인지 확인
(*result) = 258;
else if (heredoc((*list)->next)) // list에 담긴 토큰이 here_doc인지 확인
(*result) = 130;
else
return (0);
free((*line));
free_list((*list));
(*line) = 0;
return (1);
}
while(1) 안에서 자식 프로세스가 없는 메인 프로세스 상태일 때 시그널 입력을 받고, 그에 따른 errno
값을 변경해 줍니다.
read line
으로 문자열 입력값을 받은 상태에서 토큰 단위로 파악할 수 있게 파싱을 해줍니다.
'
, "
로 들어오면 잘 잘라내주고, 공백
이나 탭
이 섞여 들어오면 잘 잘라주고, $환경변수
로 들어왔는지 등등 잘 다듬어줍니다.
생선 가시 발라먹듯 잘 parsing
해주고 실행 단위로 자른 첫 번째 토큰이 명령어인지 here_doc
인 지 확인해 줍니다.
int main_single_builtin_check(t_list **list, int *result, t_copy *env)
{
if (pipe_exists((*list)->next)) // 파이프가 존재하는지 파악합니다.
return (0); // 존재한다면 if문으로 들어가 command_run을 실행합니다.
else
if (main_builtin(list, result, env))
return (1); // 파이프가 존재하지 않는다면 built_in인지 확인하기 위해 main_builtin 함수를 실행합니다.
return (0);
}
우선 파이프가 존재해서 fork()로 프로세스를 만들어야 하는지
파이프가 존재하지 않으면 메인 프로세스에서 처리가 가능하고 해당 명령어가 bulit_in
인지 확인합니다.
int command_run(t_list *list, t_copy *e, int result)
{
int pipefd[2][2];
int pid;
pipefd[NEXT][READ] = 0;
pipefd[NEXT][WRITE] = 0;
while (list)
{
command_run_fd_prev(list, pipefd);
pid = fork(); // 프로세스 생성
if (pid < 0)
{
perror("fork failed");
exit (1);
}
else if (pid == 0)
child_process(&list, e, pipefd, result); // 자식 프로세스
command_run_fd_post(pipefd);
while (list && ft_strncmp(((char *)list->content), "|", 2) != 0)
list = list->next; // |가 나올때까지 리스트를 넘겨줍니다.
if (list)
list = list->next;
}
return (status_return(pid));
}
파이프가 존재하면 파이프 개수만큼 프로세스를 생성해서 명령어를 실행시켜 줍니다.
int main_builtin(t_list **list, int *result, t_copy *env)
{
char **command;
char *temp_string;
int fd[3][2];
int tnum;
main_builtin_init(fd, &command, &temp_string); // fd 초기화 및 연결
tnum = command_split((*list)->next, fd, &command, &temp_string);
if (tnum)
{
vector_free(command);
free(temp_string);
(*result) = tnum;
return (tnum);
}
parse_expand(&command, *result, env); // 벡터로 들어온 인자들을 확인 및 정리 해줍니다.
if (!((*list)->next) || !command || !(builtin_check(command[0])))
{
free(temp_string);
vector_free(command);
return (0);
}
main_builtin_fd_mid(fd); // fd값 변경
(*result) = builtin_exec(command, env); // built_in 실행 및 errno 설정
free(temp_string);
return (main_builtin_fd_post(fd), 1);
}
파이프가 존재하지 않고 built_in
명령어일 경우 메인 프로세스에서 처리해 줍니다.
fd값을 파이프가 있을 땐 fd[0]과 fd[1]을 사용해 통신했으므로 built_in
만 존재할 땐 fd[2]에서 처리해줍니다.
void parse_expand(char ***command, int result, t_copy *env)
{
t_list *temp_list;
temp_list = vector_to_list(command);
free(*command);
ft_lstadd_front(&temp_list, ft_lstnew(0));
qmark_expansion(temp_list, result);
env_expansion(temp_list, env->cp_envp);
quote_trim(temp_list);
free_empty(temp_list);
list_tie(temp_list);
free_space(temp_list);
free_empty(temp_list);
(*command) = list_to_vector(temp_list);
free_list(temp_list);
}
2차원 백터의 주소값으로 받아 3중포인터로 매개변수를 받습니다.
parsing
된 인자들을 따옴표들을 제거해주고, 비어있거나 공백문자만 남겼으면 free해주는 등등 처리를 거쳐 토큰
으로 만들어줍니다.
void ft_signal(int signum)
{
if (signum != SIGINT)
return ;
if (rl_on_new_line() == -1)
exit(1);
printf("\n");
rl_replace_line("", 1);
rl_redisplay();
g_result = 130; // errno
}
void handle_signal(void)
{
struct sigaction new;
new.sa_flags = 0;
sigemptyset(&new.sa_mask);
new.__sigaction_u.__sa_handler = ft_signal;
sigaction(SIGINT, &new, 0); // ctrl + c 만 처리
new.__sigaction_u.__sa_handler = SIG_IGN;
sigaction(SIGQUIT, &new, 0); // ctrl + \ 는 무시합니다
}
fork()를 진행하지 않은 메인 프로세스에서 시그널 신호가 들어오면 sigaction
으로 처리해 줍니다.
프로세스를 따로 만들지 않고, signal 함수가 void형 함수만 인자로 받기 때문에 전역변수로 errno
를 관리했습니다.
지금까지 진행한 프로젝트 중 가장 오랜 시간이 걸린 프로젝트입니다.
내용이 많아 블로그에 모두 적기엔 한계가 있어 큰 틀만 설명했습니다.
리눅스 bash가 내부적으로 어떻게 동작하고 어떤 기능이 있는지 많이 알게 되었습니다.
해당 프로젝트로 구현 실력이 크게 상승하고 자료구조의 활용, 멀티 프로세스와 시그널 등 다양한 개념을 배우고 팀 프로젝트를 진행하며 고려해야 할 사항 등 많은 성장을 했다 생각합니다.
약 한 달간 진행한 프로젝트로 여러 번 갈아엎고 다시 코드를 짜고, 많은 토의를 거쳐 완성한 프로젝트로 아직 부족함이 있을지는 모르지만 뜻깊은 시간이었습니다.