Loki's Blog

로그인 쉘과 비로그인 쉘: 프롬프트가 뜨기까지 갈라지는 지점

  • CS
  • 운영체제
2025-11-23

들어가며

새 터미널 창을 열 때마다 ~/.bash_profile이 다시 실행되는 줄 알았는데, 실제로는 아니었다. 로그인할 때 읽는 설정 파일과 이미 로그인한 상태에서 새 터미널만 열 때 읽는 설정 파일이 다르다는 걸 확인하고 나니, 로그인이 정확히 어디부터 어디까지의 과정인지부터 다시 봐야겠다는 생각이 들었다. 로그인 프롬프트에 비밀번호를 치고 엔터를 누르는 순간부터 쉘 프롬프트가 뜨기까지, 그 사이에 커널과 쉘이 어떤 순서로 개입하는지 하나씩 따라가 봤다.

터미널과 쉘, 인터럽트부터 구분하고 간다

이 흐름을 보려면 터미널과 쉘부터 구분해야 한다. 뒤에 계속 나올 "로그인 쉘"이라는 말도 이 둘을 구분해야 무슨 뜻인지 감이 잡힌다. 터미널은 사용자와 운영체제 사이에서 텍스트 기반 입력과 출력을 담당하는 화면 또는 환경일 뿐이다. 키보드로 입력한 내용을 받아들이고, 프로그램이나 쉘이 출력한 결과를 그대로 화면에 보여주는 역할만 한다. 요즘 쓰는 "터미널 앱(터미널 에뮬레이터)"은 예전의 물리적인 단말기(콘솔/TTY)를 소프트웨어로 흉내 낸 것이다.

실제로 명령어를 해석해 운영체제(커널)에 전달하는 쪽은 쉘이다. 쉘은 터미널에서 넘어온 문자열을 분석해 프로그램을 실행하고, if, for, 파이프, 리다이렉션 같은 제어 흐름을 처리하는 명령어 해석기다. bash, zsh, sh, fish 등 여러 종류가 있고, 자동완성이나 히스토리, alias 같은 편의 기능과 문법이 쉘마다 다르다. 보통 터미널을 실행하면 그 안에서 기본 로그인 쉘이 자동으로 시작되어 프롬프트를 보여준다.

이 과정을 이해하려면 인터럽트 개념도 필요하다. 인터럽트는 하드웨어나 소프트웨어에서 긴급하게 CPU의 처리가 필요할 때, CPU의 현재 작업을 중단시키고 우선적으로 특정 작업을 수행하도록 하는 신호다. 장치나 소프트웨어가 인터럽트 신호를 발생시키면 CPU는 이를 감지해 현재 작업의 위치(상태)를 저장하고, OS 커널에 미리 정의된 인터럽트 처리 루틴으로 이동해 해당 이벤트를 처리한 뒤, 처리가 끝나면 저장해두었던 위치로 돌아가 원래 작업을 계속한다. 뒤에서 볼 키보드 입력도 이 인터럽트로 커널에 전달된다.

이 두 개념을 쥐고 나면, 로그인부터 프롬프트가 뜨기까지의 전체 흐름은 아래처럼 정리된다.

1. 사용자 로그인 시도

2. 계정 인증 (/etc/passwd, /etc/shadow)

3. 인증 성공 → 로그인 기록

4. 권한 설정 (UID/GID)

5. 홈 디렉터리로 이동

6. 최소 환경 변수 설정

7. exec()로 로그인 쉘 실행 (login → bash)

8. bash가 로그인 쉘로 시작 (-bash)

9. 시스템 전역 설정 실행
   - /etc/profile
   - /etc/profile.d/*.sh
   - /etc/bash.bashrc

10. 사용자 설정 실행
    - ~/.bash_profile (또는 ~/.bash_login, ~/.profile)
    - ~/.bashrc

11. 프롬프트 표시

12. 사용자 작업 시작

1. 계정 인증부터 시작된다

사용자가 로그인 프롬프트에 사용자명과 비밀번호를 입력하는 순간부터 이미 커널이 개입한다. 키보드를 누르면 하드웨어 장치에 전기적 신호로 전달되고, 이 입력 신호가 들어오면 커널이 이를 인터럽트로 감지한다. 프로세스는 입력 버퍼를 읽는 시스템 콜로 한 글자(또는 한 줄)씩 값을 가져오고, 입력 값을 실시간으로 화면에 출력한다.

입력이 끝나면 시스템은 두 파일을 근거로 계정을 확인한다. 먼저 /etc/passwd 파일에서 입력한 사용자명이 시스템에 존재하는지 확인하고, UID / GID / 홈 디렉터리 / 기본 쉘 경로 같은 사용자 정보를 읽는다.

username:x:1000:1000:Full Name:/home/username:/bin/bash

비밀번호는 /etc/shadow 파일로 검증한다. 입력한 비밀번호를 같은 해시 알고리즘으로 암호화한 뒤, 저장되어 있는 암호화된 값과 비교해 일치 여부를 확인하는 방식이다.

username:$6$salt$hash:19000:0:99999:7:::

인증에 실패하면 오류 메시지를 출력하고 다시 로그인 프롬프트로 돌아가며, 실패 기록은 /var/log/auth.log에 남는다. 반대로 인증에 성공하면 시스템은 여러 로그 파일에 흔적을 남긴다.

/var/run/utmp에는 현재 로그인한 사용자 정보가 실시간 / 휘발성으로 기록되고, /var/log/wtmp에는 모든 로그인 / 로그아웃 이력이 영구 보관되며, /var/log/lastlog에는 각 사용자의 마지막 로그인 정보가, /var/log/auth.log(또는 /var/log/secure)에는 인증 관련 상세 로그가 쌓인다. 누가 언제 로그인 / 로그아웃 했는지가 남아 있으면 침입 시도를 분석하거나 장애 직전 사용자의 작업 기록을 추적할 때, 또는 여러 운영자 중 누가 어떤 변경을 했는지 감사할 때 이 로그를 근거로 쓴다.

2. 로그인 프로그램이 쉘 실행을 준비한다

인증이 끝났다고 곧바로 쉘이 뜨는 건 아니다. 그 사이에 로그인 프로그램이 사용자의 프로세스 권한과 환경을 준비하는 단계가 있다. 여기서 실행될 대상이 바로 로그인 쉘이다. 로그인 쉘이란 사용자가 시스템에 로그인했을 때 처음으로 실행되는 쉘 프로그램을 말한다. 사용자의 기본 로그인 쉘은 /etc/passwd 파일의 마지막 필드에 정의되어 있으며, /bin/bash, /bin/zsh 등이 그 값이 될 수 있다.

이 쉘을 실행하기 전에 로그인 프로그램은 먼저 프로세스의 권한을 사용자 것으로 바꾼다. setgid() 시스템 콜로 그룹 ID를, setuid() 시스템 콜로 사용자 ID를 설정하면 이제 프로세스는 해당 사용자의 권한으로 실행된다. 여기에 더해 /etc/group 파일을 읽어 사용자가 속한 모든 보조 그룹을 확인하고, setgroups() 시스템 콜로 그 권한까지 설정한다.

권한을 다 갖춘 뒤에는 chdir() 시스템 콜로 사용자의 홈 디렉터리로 작업 디렉터리를 옮기고(예: chdir("/home/username")), 로그인 프로그램이 최소한의 환경 변수만 미리 설정해둔다.

HOME=/home/username      # 홈 디렉터리
USER=username            # 사용자 이름
LOGNAME=username         # 로그인 이름
SHELL=/bin/bash          # 기본 쉘
PATH=/usr/local/bin:/usr/bin:/bin    # 기본 실행 경로
TERM=xterm               # 터미널 타입

마지막으로 새로운 세션 ID를 할당하고 터미널(TTY)을 연결하고 프로세스 그룹을 설정하면, 쉘을 실행할 준비가 끝난다.

3. exec()가 프로세스를 통째로 쉘로 바꿔치기한다

준비가 끝나면 로그인 프로그램은 exec() 시스템 콜로 /etc/passwd에 지정된 쉘을 실행한다.

// 의사 코드
char *shell = "/bin/bash";  // /etc/passwd에서 읽어온 쉘 경로
execl(shell, "-bash", NULL);  // "-bash"는 로그인 쉘임을 의미

exec()는 현재 프로세스를 새 프로그램으로 완전히 대체하는 시스템 콜이라, 로그인 프로그램(login)은 종료되고 같은 PID로 bash가 실행된다. PID는 그대로 유지되지만 그 PID가 가리키는 실행 중인 프로그램은 완전히 바뀌는 셈이다.

여기서 넘긴 인자가 뒤의 모든 차이를 가른다. "-bash"처럼 첫 번째 인자(argv[0])가 하이픈(-)으로 시작하면 쉘은 스스로를 로그인 쉘로 인식한다. bash, zsh, sh 모두 이 관례를 따르고, 로그인 쉘은 일반 쉘과는 다른 초기화 과정을 거친다. 로그인 프로그램이 따로 존재하거나 별도의 플래그를 넘기는 게 아니라, 인자 문자열 맨 앞의 글자 하나가 이후 실행될 초기화 경로 전체를 결정하는 셈이다.

4. 로그인 쉘이 시스템 전역 설정부터 읽는다

로그인 쉘로 시작된 bash는 자동으로 환경을 초기화하는데, man7.org에 공개된 bash 매뉴얼(INVOCATION 절)에 따르면 로그인 쉘은 가장 먼저 /etc/profile이 존재하면 그 파일을 읽고 실행한다. 모든 사용자에게 공통으로 적용되는 설정으로, PATH나 로케일, 히스토리 크기 같은 시스템 전체 환경 변수를 담고 있다.

  # PATH 설정
  PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
 
  # 로케일 설정
  LANG="ko_KR.UTF-8"
 
  # 히스토리 크기
  HISTSIZE=1000
  HISTFILESIZE=2000

배포판에 따라 다르지만, 데비안 / 우분투 계열에서는 /etc/profile/etc/profile.d/ 디렉터리 안의 .sh 파일들을 자동으로 실행하도록 되어 있어 color.sh, java.sh처럼 모듈화된 설정을 관리할 수 있다. 이어서 bash 쉘 전용 전역 설정인 /etc/bash.bashrc가 실행되어 프롬프트 색상이나 기본 alias 같은 값을 모든 bash 세션에 적용한다. 다만 /etc/bash.bashrc는 GNU bash 자체가 아니라 데비안 계열 배포판이 붙인 확장이라, 다른 배포판에서는 없을 수 있다.

5. ~/.bash_profile은 로그인할 때만, ~/.bashrc는 매번

시스템 전역 설정이 끝나면 로그인 쉘은 사용자 홈 디렉터리로 눈을 돌린다. 같은 bash 매뉴얼에 따르면 로그인 쉘은 ~/.bash_profile, ~/.bash_login, ~/.profile 순서로 찾아보고, 그중 가장 먼저 존재하고 읽을 수 있는 파일 하나만 실행한다.

가장 우선순위가 높은 ~/.bash_profile이 흔히 쓰이는 파일이다. 환경 변수를 설정하고, 보통 그 안에서 ~/.bashrc를 호출하는 식으로 구성한다.

# ~/.bash_profile 예시
# 환경 변수 설정
export EDITOR=vim
export PATH="$HOME/bin:$PATH"
 
# ~/.bashrc 호출 (일반적)
if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi

~/.bash_profile이 없을 때만 ~/.bash_login이 실행되는데, 이는 구형 시스템과의 호환성을 위해 남아 있는 파일이다. 앞의 두 파일이 모두 없으면 마지막으로 ~/.profile이 실행되며, 이 파일은 POSIX 표준 호환 파일이라 sh나 dash 같은 다른 쉘과도 함께 쓸 수 있다.

~/.bash_profile이 대개 그 안에서 호출하는 ~/.bashrc는 대화형 쉘 설정, alias와 함수 정의, 프롬프트(PS1) 커스터마이징, 쉘 옵션 설정을 담당한다.

# ~/.bashrc 예시
# Alias 설정
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
 
# 함수 정의
mkcd() {
    mkdir -p "$1" && cd "$1"
}
 
# 프롬프트 커스터마이징
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
 
# 쉘 옵션
shopt -s histappend  # 히스토리 추가 모드
shopt -s checkwinsize  # 윈도우 크기 확인
 
# 히스토리 설정
HISTSIZE=10000
HISTFILESIZE=20000
HISTCONTROL=ignoredups:ignorespace

여기서 처음 궁금했던 지점이 풀린다. bash 매뉴얼은 로그인 쉘이 아닌 대화형 쉘, 즉 이미 로그인한 상태에서 새 터미널 창을 열었을 때는 ~/.bash_profile 계열을 전혀 읽지 않고 ~/.bashrc만 읽는다고 명시한다.

새 터미널을 열 때는 login이 다시 계정을 인증하고 exec()"-bash"를 실행하는 절차를 밟지 않는다. 그 대신 argv[0]에 하이픈이 없는 "bash"가 새로 뜨면서 시스템 전역 설정과 ~/.bash_profile 체인을 건너뛴다. 그래서 ~/.bash_profile에만 넣어둔 환경 변수는 새 터미널 탭에서 보이지 않는다.

6. 모든 설정이 끝나면 프롬프트가 뜬다

시스템 전역 설정과 사용자 설정 파일 실행이 모두 끝나면 비로소 사용자가 명령어를 입력할 수 있는 상태가 된다. 쉘 프롬프트가 화면에 표시된다.

username@hostname:~$

이 시점부터 쉘은 명령어를 읽고(Read), 실행하고(Eval), 결과를 출력하고(Print), 다시 반복하는(Loop) REPL로 동작하기 시작한다.

정리하며

결국 로그인 쉘인지 아닌지는 로그인이라는 행위 자체가 아니라, 쉘을 실행할 때 넘긴 argv[0]가 하이픈으로 시작하느냐는 관례 하나로 갈린다. 로그인 직후 뜨는 쉘은 loginexec()"-bash"를 넘겨 실행되고, /etc/profile부터 ~/.bash_profile까지 전체 체인을 읽는다.

이미 로그인한 상태에서 새 터미널 창을 열 때는 그 인증 / 권한 설정 절차를 다시 밟지 않는다. argv[0]에 하이픈이 없는 bash, 즉 비로그인 쉘 하나가 새로 뜨면서 ~/.bashrc만 읽을 뿐이다. 새 터미널에 넣은 환경 변수가 갑자기 안 보인다면, ~/.bash_profile에만 넣어뒀는지부터 확인하면 된다.

관련 글