computer science

운영체제 Shell기능 직접구현해보기(MyShell.c)

갬짱 2024. 3. 10. 17:24

 

23.09.25

CentOS(Linux), putty 이용

 

fork() : 존재하는 프로세스(부모)와 동일한 메모리를 가진 새로운 프로세스(자식)를 생성하여 수행한다.

-> 2가지 제어흐름으로 진행 (system’s view)

-> 2개의 값 반환(부모: child’s pid / 자식: 0) (program’s view)

 

 

wait() : 자식 프로세스가 종료될때까지 대기한다. 자식pid를 반환한다.

자식이 정상적으로 종료되었다면 인자 &status의 2번째 바이트에 반환값 저장

# 정상종료 : 상위바이트(프로세스 반환값) + 하위바이트(0)

# 비정상종료 : 상위바이트(0) + 하위바이트(종료시킨 시그널 번호)

 


 

execve() : execute a new program(binary loading)

  • 현재 실행메모리(text,data,stack)을 새롭게 대체한다. ( * fork와의 차이 )
  • 일종의 loader 역할을 수행한다.

*인자 타입별

execlp, execl, execle : 인자가 list → vector 함수로 빌드됨

execvp, execv, execve : 인자가 vetor인 함수

*인자 내용별

execlp, execvp : 인자에 경로(pathname)을 포함(별도기입X)

execle, execve : 인자에 환경변수(char *const envp[])를 넣을 수 있음

 

execvp( const char *filename, char *const argv[]);

- filename : 실행할 프로그램 파일명 포인터

- argv[] : 실행할 벡터전체( 프로그램 파일, 인자등이 모두 저장 )

 


 

[ Shell ]

: user의 명령어를 수행한다. (command interpreter)

 

*Basic logic

  1. display prompt
  2. input parsing(분해)
  3. external commands (외부명령어) : 외부 디렉토리 실행파일을 실행하는 개념 / fork -> exec 를 통해 복제한 자식을 자기대치시킴 <-> internal commands (내부명령어) : 쉘의 일부 / fork, exec 이용없이 동작

+) background processing(&), redirection(>,<), pipe(|)

쉘에서 외부명령어의 동작방식

 

 

  • 나만의 shell 프로그램 구현(myshell.c)

char* getcwd( char* buf, size_t size ) : 현재 작업경로를 반환한다.

char* fgets( char* string, int n, FILE *stream) : 사용자가 입력하는 문자열을 반환한다.

* main함수 : fget()을 통해 line에 명령을 입력받음

* run( line ) : ls -al 명령을 토큰화 & fork()하여 얻은 자식 프로세스에서 execvp로 명령실행

ctrl + c : 프로그램 강제종료로 루프탈출

 


 

  • Tokenizing : 명령문자열을 parsing하여 execvp()에서 활용하기 위함

char* strtok( char* str, char* delimiters )

-> strtok(문자열, delimiters) : 해당문자열을 구분자로 분리(1회)

-> strtok(NULL, delimiters) : 직전에 찾은 구분자 뒤에서 새로운 문자를 찾음(n회), NULL(\0)(문자열의 끝)을 만나기 전까지 실행

* tokenize( line, delims, tokens[], maxTokens )

: 입력문자열, 구분자, 토큰배열(토큰을 저장), 최대배열의 크기(최대 토큰갯수)을 입력 / 만들어낸 토큰의 갯수를 출력

-- strtok(buf, delims)로 문자열을 1회 토크나이징

-- 토큰이 null이 아니고( token !=NULL ) 토큰의 갯수가 최대토큰갯수보다 작다면

배열에 저장 -> 토큰카운트++ -> 문자열 끝까지 계속토크나이징( strtok(NULL, delims) )

-- 마지막 토큰은 NULL로 채움 tokens[token_count] = NULL

 

*run(line) : 반환받은 token_count가 0이라면(없음) 종료 / 아니라면 자식프로세스에서 exevp()함수에 토큰배열을 이용하여 명령수행

 

 


 

  • Built-in-command : 쉘 그자체에 포함된 명령어 -> 다른 프로그램을 invoke할 필요없다.
BASH쉘에서 제공하는 builtin명령들

 

* 구조체 COMMAND의 배열 builtin_cmds[] : builtin명령들의 이름, 상세설명, 포인터함수(*func)를 담아둔다.

* execute_builtin_command( tokens, token_count )

: 명령어를 토크나이징한 tokens배열, token갯수를 입력 / 해당 빌트인함수 실행(성공수행시 1리턴), 빌트인함수 아닐시(없음) 0리턴

-- builtin 구조체배열의 인자수만큼 for문을 반복탐색 sizeof(builtin_cmds)/sizeof( struct COMMAND)

-- 해당하는 builtin명령이라면 ( =builtin 구조체 이름과 첫번째 토큰명이 일치한다면 ) strcmp(builtin_cmds[i].name, tokens[0]) == 0

해당빌트인함수를 실행 return builtin_cmds[i].func(token_count, tokens) (빌트인함수로 제어권이동하여 실행성공시 1반환)

 

* cmd_help( int argc, char* argv[] )

-- builtin 구조체배열의 인자수만큼 for문을 반복탐색 sizeof(builtin_cmds)/sizeof( struct COMMAND)

-- 만약 help만 입력했거나(= token배열의 수가 1개), 해당하는 builtin이름과 help XX가 동일하다면(=token[1]의 값)

if (argc == 1 || strcmp(builtin_cmds[i].name, argv[1]) == 0)

builtin구조체에서 이름과 상세설명을 출력 printf("%-10s: %s\n", builtin_cmds[i].name, builtin_cmds[i].desc);

* cmd_cd( int argc, char* argv[] )

--cd만 입력했다면( argc==1 ) 최상위디렉토리로 이동 chdir(getenv("HOME"));

--cd XX로 입력했다면 ( argc==2 ) 해당하는 곳으로 이동 chdir(argv[1])

* cmd_exit( int argc, char* argv[] )

--return -1

 

*run(line): built함수가 정상적으로 실행된경우(1) -> 1을 리턴하고 run종료(내부명령, fork할 필요없음)

built함수가 아닌경우(0) 아래 명령 지속..

 


 

  • Background processing(&) : 쉘과 사용자 명령, 프로그램이 동시에 수행된다(run concurrently)

<-> foreground processing : 쉘의 프로세스가 종료될때까지 사용자는 명령어를 입력, 다른 프로그램수행하지 못한다. (동기적 실행)

 

 

HOW? 복제한 프로세스가 wait()하지 않음으로써 구현할 수 있다.

*handle_background( tokens, token_count ) : 명령어 토큰에 &(background명령)이 존재하면 true반환

--명령어 마지막위치에 &가 존재한다면 ( 마지막위치로 제한 )

if (strcmp(tokens[token_count - 1], "&") == 0)

--해당위치를 NULL로 변환 (&은 명령어 토큰 공간에 남겨두지 않고 여기서 확인하고 처리) tokens[token_count - 1] = NULL;

 

*run(line) : 부모 프로세스 공간에서 만약 background명령확인결과 없다면 if (!is_background) 일반적으로 대기 wait(NULL)

아니라면 기다리지 않고 진행, 자식 pid 출력 printf("[BG] %d\n", child);

 


 

  • Redirection(>) : STDIN/STDOUT(표준입출력) 대신 파일에 읽고 쓰는 작업

HOW? execve()이전에 dup2()함수를 이용해서 STDIN/STDOUT의 fd가 다른 특정 file의 fd를 가르키도록 변환한다.

int dup2( int oldfd, int newfd ) : newfd가 oldfd를 참조하도록 한다.

file descripter의 기본구조 : 0 = STDIN_FILENO , 1 = STDIN_FILENO, 2 = STDERROR_FILENO ( 고정적인 fd ) / 다른 파일들은 open작업을 통해 3번이후의 fd를 할당받는다.

 

*handle_redirection( tokens, token_count ) : 명령토큰내 ">"가 존재하면 target파일을 open하여 fd를 반환한다.

--for문을 돌려 토큰배열 내에 ">"가 존재하는지 찾는다

--target파일을 입력하지 않은 경우( >의 위치인덱스가 마지막 토큰 ) i == token_count - 1 -----> 경고안내문 출력

--target파일 open작업(존재하지 않으면 생성) open(tokens[i + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);

--open이 실행되지 않은경우 if (fd_out == -1) 에러출력하고 종료

--해당위치를 NULL로 변환 ( >은 명령어 토큰 공간에 남겨두지 않고 여기서 확인하고 처리 -> 이후 앞의 명령어만 해석될 수 있도록함

tokens[i] = NULL;

--fd를 반환 ( 존재하지 않는다면 0 ) return fd_out;

 

*run(line) : execve() 실행전에 dup2()를 통해 명령어 수행이 반환받은 fd_out에 출력될 수 있게 한다. dup2(fd_out, STDOUT_FILENO);

 


 

**pipe는 생략