개발놀이터

리눅스 기본 명령어 동작 원리 : 디렉토리 구조편 (cd, ls, rm, mv, cp) 본문

CS 지식/운영체제

리눅스 기본 명령어 동작 원리 : 디렉토리 구조편 (cd, ls, rm, mv, cp)

마늘냄새폴폴 2024. 10. 29. 23:10

이번 포스팅은 이전 포스팅에서 영감을 얻었습니다. 

 

https://coding-review.tistory.com/563

 

데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까?

오랜만에 포스팅을 쓰는 것 같습니다. 거의 열흘만인 것 같은데 요즘 쿠버네티스를 공부하느라고 실습을 위주로 공부하고 있느라 공부할 시간이 마땅히 나지 않았네요...  이번 포스팅은 평소

coding-review.tistory.com

제 공부 방향은 항상 문득 떠오르는 주제이거나 이전 주제에서 깊이있게 공부해볼만한 주제를 따로 선정해서 더 공부하는 편입니다. 이전에 데이터베이스 쿼리를 실행하면 OS레벨에서 어떤 일이 벌어지는지 공부했는데 이때 System Call이나 파일시스템에 대해서 많은 공부가 되었고 이 내용을 잘 활용하면 기본 명령어도 설명할 수 있겠다 싶어서 공부해봤습니다. 

 

이번 포스팅에선 기본 명령어 동작 원리, 그 중에서도 디렉토리 구조와 관련된 기본 명령어 cd, ls, rm, mv, cp에 대해서 공부해보고 정리한 내용을 다루려합니다. 

 

리눅스와 디렉토리 구조

리눅스는 참 재밌는 디렉토리 구조를 가지고 있는데 inode와 directory entry (이하 디렉토리 엔트리) 라는 것을 이용합니다. 

  • inode : inode는 파일의 메타데이터를 저장하고 있는 노드로서 파일 이름, 권한, 생성일 등이 적혀있습니다. 
  • directory entry : 우리가 조작하려는 파일이 어떤 디렉토리에 속해있는지 적혀있습니다. 

inode야 그렇다쳐도 디렉토리 엔트리는 어디에 쓰는 것일까요? 

 

예를 들어서 한가지 상황을 가정해보겠습니다. 

 

만약 A라는 파일이 /pollpoll 이라는 디렉토리에 존재하고 있다면 cd pollpoll을 입력하는 순간 현재 디렉토리 엔트리를 /pollpoll로 바꿔서 그 곳을 바라보게 됩니다. 이 상태에서 ls 명령어를 입력한다면 /pollpoll 의 디렉토리 엔트리를 가지고 있는 목록을 보여주게됩니다. 

 

즉, 파일과 디렉토리 엔트리가 서로 매핑되어 있다는 것이죠. 이렇게 하면 무슨 이점이 있을까요?

 

바로 파일의 직접적인 접근을 최소화할 수 있습니다. 

 

직접 파일을 조작해야하는 rm, cp와 같은 명령어를 제외하면 실제 데이터를 조작하지 않고도 파일을 변경할 수 있는 것이죠. 

 

여기서 눈치채신 분도 있으시겠네요. 맞습니다. mv의 경우 디렉토리 엔트리만 변경해 실제 데이터의 이동 없이 디렉토리를 변경할 수 있습니다. 이렇게 함으로써 데이터의 실제 이동 없이 마빡에 붙어있는 자신의 위치만 변경해주게 됩니다. 

 

사실 mv를 쓰면서도 굉장히 빠른 성능에 의아했는데 윈도우를 생각해보면 대용량 파일의 디렉토리 이동은 거의 재앙과 같기에 의아했었지만 이런 숨겨진 동작이 있었을줄은 몰랐습니다. 이 부분은 조금 뒤에 더 자세히 풀어보겠습니다. 

 

리눅스 기본 명령어 동작 방식

cd

리눅스 터미널에서 cd명령어가 입력되면 리눅스 커널은 chdir()이라는 System Call을 호출하는데 이름에서부터 알 수 있듯이 디렉토리 엔트리를 변경하는 System Call입니다. 

 

System Call에 대한 자세한 내용은 아래의 링크에 자세히 설명되어있으니 한번 들러보시는 것도 좋을 것 같습니다!

 

https://coding-review.tistory.com/563

 

데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까?

오랜만에 포스팅을 쓰는 것 같습니다. 거의 열흘만인 것 같은데 요즘 쿠버네티스를 공부하느라고 실습을 위주로 공부하고 있느라 공부할 시간이 마땅히 나지 않았네요...  이번 포스팅은 평소

coding-review.tistory.com

 

System Call과 함께 리눅스 커널은 CPU를 커널모드로 진입시키고 커널모드 상태에선 시스템 자원에 접근할 수 있는데 이 때 프로세스에 접근합니다. 커널모드에서 현재 이 디렉토리에 접근해도 되는 사용자인지 권한을 체크하고 만약 허가되면 프로세스의 타겟을 현재 디렉토리로 변경합니다. 

 

프로세스가 변경되면서 프로세스의 메타데이터도 업데이트하게 되고 이 때 프로세스가 타겟을 현재 디렉토리로 변경하지만 실제 디스크에 존재하는 파일이 변경되거나 하진 않습니다. 

 

즉, 정말 디렉토리 엔트리만 변경하는 것이죠. 

 

ls

ls 명령어가 입력되면 open()과 getdents()라는 System Call을 호출합니다. 이 순간 cd와 마찬가지로 커널모드로 진입하고 커널은 현재 파일의 inodes를 읽습니다. 

 

리눅스 커널은 커널모드로 진입하면서 디렉토리의 내용물들을 조회하고 이 때 시스템 자원인 파일시스템의 메타데이터에서 디렉토리 엔트리를 읽어옵니다. 

 

디렉토리 엔트리를 가져오고 나서 커널은 디렉토리에 있는 정보들을 사용자 화면에 뿌려주기 위해 유저모드로 전환합니다. 이 때 유저가 -l이나 -a같은 옵션을 넣었다면 그것에 맞는 데이터를 보여줍니다. 

 

ls의 재밌는 특징 중 하나는 사용자가 여러번 ls 명령어를 썼을 때를 대비해 현재 디렉토리 엔트리를 캐싱해놓습니다. 개발자들이 습관적으로 ls를 입력한다는 사실을 아는 것 같은 재밌는 특징이네요. 

 

rm

rm 명령어를 사용하면 unlink()라는 System Call을 호출합니다. 해당 System Call이 호출되면 서로 연결되어있던 파일과 디렉토리 엔트리의 관계가 끊어지게 되고 inode도 이때 삭제해버립니다. 

 

inode가 삭제되면서 커널은 해당 파일에 "지워짐" 이라는 마크를 달아놓습니다. 

 

커널은 이후 파일시스템의 메타데이터를 업데이트하면서 해당 파일을 완벽하게 지울 준비를 합니다. 또한, 캐시를 즉시 업데이트 파일을 조회할 수 없게 바꿔버리는데 실제 파일을 디스크에서 삭제하는 것 보다 캐시를 먼저 삭제하는 것은 혹시라도 파일이 삭제되는 과정에서 조회하는 경우 파일이 보이는 것을 방지하기 위함입니다. 

 

캐시를 삭제한 뒤 파일시스템에 접근하고 CPU는 Storage Controller에게 실제 데이터를 지우라고 명령합니다. 그렇게 되면 HDD의 경우 스토리지 컨트롤러에 의해 헤드가 물리적으로 이동하게 되고 실제 데이터가 있는 곳을 찾아가 파일을 지워버립니다. SSD의 경우 해당 파일을 지우러나느 전기신호를 받고 파일을 스토리지 디바이스에서 지워버립니다. 

 

mv

mv 명령어를 실행하면 rename()이라는 System Call을 호출하는데 이 rename()이라는 System Call은 굉장히 중의적입니다. 

 

실제로 mv 명령어를 사용하는 경우는 파일의 이름을 변경할 때나 파일의 위치를 변경하는 경우인데 이 경우 둘 다 rename이라는 이름이 잘 어울리기 때문이죠. 

 

먼저 파일의 이름을 변경하는 경우 커널이 rename()을 호출함과 동시에 커널모드로 진입하고 파일시스템에 접근해 메타데이터에 존재하는 파일 이름을 변경해버립니다. 진짜 파일의 이름을 변경하는 것이기에 rename이라는 이름이 잘 어룰리죠. 

 

또한, 파일의 위치가 변경되는 경우 마찬가지로 커널모드로 진입해 파일시스템의 메타데이터에서 디렉토리 엔트리를 읽어와 디렉토리 엔트리를 변경해버립니다. 디렉토리 엔트리를 바꾸는 것이기에 이 것 또한 잘 어울리네요. 

 

mv의 특이한 점이라면 파일의 실제 변경 없이 이루어지기 때문에 성능상 엄청난 이점을 가지고 있다는 것이죠. 

 

cp

cp 명령어는 꽤나 많은 System Call이 호출됩니다. 

 

  1. open() : 파일을 여는 System Call입니다. 
  2. read() : 복제할 파일의 바이트를 읽어오고 이후 write()를 호출합니다. 
  3. write() : 명령어에 적힌 디렉토리 파일로 파일을 씁니다. read()와 write()는 짝을 이루어 실행된다는 특징이 있습니다. 
  4. chmod() : 파일이 복제되고 난 뒤 파일의 권한도 복제하기 위해 chmod()를 호출합니다. 이 System Call에 대해서는 다음 포스팅에서 자세히 다룰 예정입니다. 
  5. utime() or utimes() : 파일의 메타데이터에 생성일을 적기 위해 필요한 System Call인 utime혹은 utimes를 호출합니다. 
  6. close() :  open()을 이용해서 파일을 열 때 가져왔던 자원들을 다시 시스템에게 돌려주기위해 close()를 호출합니다. 

 

마치며

이번 포스팅에선 리눅스의 기본 명령어인 cd, ls, rm, mv, cp가 어떻게 커널에서 실행되는지 알아봤습니다. 30년 전 고대 개발자들이 쌓아놓은 결과물들을 보자니 정말 입이 떡벌어지는 것 같네요. 

 

이 내용을 공부하기 위해서 찾아보다가 흥미로운 내용이 있었는데요 1997년 토르발드에 의해 만들어진 리눅스는 초기엔 1만줄정도의 코드를 가지고 있었다가 현재는 330만줄에 달하는 엄청난 규모로 성장했다고 합니다. 

 

이렇게 성장하고 업계 표준이 될 수 있었던 이유 중 가장 큰 이유로 오픈소스를 꼽는데요. 지금의 AI산업에서 후에 Meta의 라마가 30년 뒤 업계 표준이 될지 한번 지켜보도록 하겠습니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!