,

[개념] fork — 프로세스·저장소·프로젝트에서 복제되는 것은 각각 무엇인가

한 줄 요약: fork 라는 한 단어가 프로세스·깃 저장소·오픈소스 프로젝트에서 각각 무엇을 복제하는지 CoW 실측(256 MB 자식의 Private_Dirty 36 kB)·GitHub API·실제 포크 사례로 갈라 봤습니다 — 셋 다 복제되는 순간이 아니라 갈라진 뒤 원본과 무엇을 주고받는가가 진짜 내용이라는 이야기입니다.

이 글의 위치: 기술 블로그 → 개념(Concepts) · ‘이름으로 읽는 개발 개념’ 시리즈 1편.
작성·검증 방식은 Editorial Policy 를 참고하세요.
본문의 모든 측정값은 debian:bookworm-slim(커널 6.12.76-linuxkit aarch64, glibc(GNU C Library) 2.36, gcc(GNU Compiler Collection) 12.2.0) 컨테이너에서 직접 실행한 결과입니다(패키지 메타데이터 비교 한 곳만 ubuntu:24.04를 함께 씁니다). 깃허브 메타데이터는 GitHub REST API 응답 원본입니다.


하루에 같은 단어를 세 번 다르게 썼습니다

어느 날 오후에 이런 일이 연달아 있었습니다.

  1. 컨테이너에서 프로세스가 안 뜨길래 로그를 봤더니 fork: Resource temporarily unavailable
  2. 깃허브에서 남의 저장소를 쓰려고 Fork 버튼을 눌렀고
  3. 저녁에는 제가 늘 쓰는 데비안 이야기가 나왔는데, 우분투와의 관계를 설명하면서 "우분투는 데비안에서 포크된 배포판"이라고 말했습니다

세 번 다 fork였는데, 세 번 다 다른 것을 복제하고 있었습니다. 그때 든 생각은 "같은 단어를 쓰는 게 우연일까, 아니면 정말 같은 동작일까"였습니다.

결론부터 적으면 같은 동작이 아니라 같은 모양입니다. 셋 다 "하나를 둘로 가른다"는 모양은 같은데, 갈라진 뒤에 원본과 무엇을 주고받는가가 전부 다릅니다. 그리고 실무에서 헷갈리는 지점은 항상 이 뒤쪽에 있었습니다.

이 글은 그 세 층위를 하나씩 직접 확인한 기록입니다.


0. 단어부터 — fork는 복제가 아니라 갈래다

fork는 라틴어 furca(갈퀴, 쇠스랑)에서 왔습니다. 지금도 식탁의 포크와 갈림길(a fork in the road)이 같은 단어입니다. 여기서 중요한 건 어원이 ‘복사’가 아니라 ‘갈라짐’ 이라는 점입니다.

이 한 글자 차이가 세 층위를 관통합니다. 복사는 결과물 두 개가 남는 일이고, 갈라짐은 하나였던 것이 두 경로가 되는 일입니다. 아래 세 층위 모두, 흥미로운 부분은 복사되는 순간이 아니라 갈라진 다음에 있습니다.


1. 층 ① 프로세스 — 한 번 부르고 두 번 리턴하는 함수

fork(2)(뒤의 2는 man 페이지 2절, 즉 시스템 콜이라는 표시입니다)는 제가 아는 함수 중 유일하게 한 번 호출했는데 두 번 리턴하는 함수입니다. 말로만 들으면 이상해서 직접 짰습니다.

int counter = 100;              // 전역: fork 뒤 서로 독립인지 확인용
char buf[64] = "parent-owned";

int main(void) {
    printf("[before] pid=%d ppid=%d counter=%d &counter=%p\n", ...);
    pid_t rv = fork();          // 여기서부터 두 갈래

    if (rv == 0) {              // 자식은 0을 받는다
        counter += 1;
        strcpy(buf, "child-wrote");
        printf("[child ] ...");
    } else {                    // 부모는 자식의 PID를 받는다
        waitpid(rv, &st, 0);
        printf("[parent] ...");
    }
}

실행 결과입니다.

[before] pid=4109 ppid=4099 counter=100 &counter=0xaaaadb750060
[child ] pid=4110 ppid=4109 fork()=0    counter=101 &counter=0xaaaadb750060 buf=child-wrote
[parent] pid=4109 ppid=4099 fork()=4110 counter=100 &counter=0xaaaadb750060 buf=parent-owned
[parent] child exited with 0

세 줄에 이 글의 절반이 들어 있습니다.

  • 리턴값이 갈래를 가릅니다. 자식은 0, 부모는 자식의 PID(Process ID, 여기서는 4110)를 받습니다. 코드는 한 벌인데 실행 경로가 둘로 갈라지는 지점이 정확히 이 반환값입니다.
  • 주소가 같습니다. 부모도 자식도 &counter가 0xaaaadb750060입니다. 그런데 값은 100과 101로 다릅니다. 같은 주소가 다른 값을 갖는 것은 그 주소가 가상 주소이고, 두 프로세스가 같은 가상 주소를 서로 다른 물리 페이지로 번역하고 있기 때문입니다.

두 번째 항목이 곧 CoW(Copy-on-Write, 쓸 때 복사) 입니다. 이름은 많이 들었는데 눈으로 본 적은 없어서, 이번엔 그것도 재봤습니다.

복제는 얼마나 복제되나 — CoW를 눈으로 보기

RSS(Resident Set Size, 실제 메모리에 올라와 있는 크기)로는 잘 안 보입니다. RSS는 공유 페이지도 양쪽에 다 세기 때문입니다. 그래서 /proc/self/smaps_rollup(smaps는 프로세스의 메모리 매핑별 통계, _rollup은 그것을 프로세스 단위로 합산한 값)의 Shared_Dirty(다른 프로세스와 공유 중인, 수정된 페이지)와 Private_Dirty(이 프로세스만 갖는, 수정된 페이지)를 봤습니다. 256MB를 할당해 전부 채운 뒤 fork하고, 자식이 앞쪽 절반만 다시 쓰게 했습니다.

fork 전 (부모)          Shared_Dirty=      0 kB   Private_Dirty= 262232 kB
  fork 직후 (자식)      Shared_Dirty= 262200 kB   Private_Dirty=     36 kB
  절반만 쓴 뒤 (자식)   Shared_Dirty= 131124 kB   Private_Dirty= 131112 kB
자식 종료 후 (부모)     Shared_Dirty=      0 kB   Private_Dirty= 262236 kB
  • fork 직후 자식이 자기 것으로 가진 메모리는 36kB입니다. 256MB를 통째로 물려받은 것처럼 보이지만 실제로는 부모 페이지를 가리키고만 있습니다.
  • 앞쪽 절반에 쓰자 정확히 절반만(131MB) 자기 것으로 넘어왔습니다. 나머지 절반은 여전히 공유입니다.
  • CoW는 "안 복사한다"가 아니라 "쓰는 만큼만, 쓰는 순간에 복사한다" 입니다.

그래도 공짜는 아니다

여기서 한 발 더 들어가면 흔한 오해가 하나 걸립니다. "CoW니까 fork는 메모리 크기와 무관하게 빠르다"는 말인데, 재보면 그렇지 않습니다.

부모가 쥔 메모리 fork() 호출에 걸린 시간
16 MB 0.434 ms
256 MB 1.847 ms
1,024 MB 9.883 ms

메모리가 64배가 되니 fork도 23배 느려졌습니다. 데이터는 복사하지 않지만 페이지 테이블은 복사해야 하기 때문입니다. 주소 공간이 클수록 옮겨 그려야 할 지도가 커집니다.

이 숫자는 실무에서 한 번쯤 부딪히는 지점이기도 합니다. 힙을 크게 쥔 프로세스에서 외부 명령 실행을 반복하면(system 계열 호출), 명령 자체보다 fork 비용이 먼저 눈에 띄기 시작합니다. 그럴 때 posix_spawn(POSIX, Portable Operating System Interface — 유닉스 계열 공통 규격이 정한 "복제 없이 바로 실행" 함수)이나 미리 띄워 둔 워커가 답이 되는 이유가 위 표에 있습니다.

참고로 아무것도 안 쥔 상태에서의 처리량은 이랬습니다.

fork만     3000회 : 0.325 초  (1회당 0.108 ms, 초당 9,221회)
fork+exec  3000회 : 0.624 초  (1회당 0.208 ms, 초당 4,811회)

초당 9천 번 복제됩니다. fork 폭탄이 왜 위험한지가 이 한 줄로 설명됩니다. 복제가 싸기 때문에 위험합니다.

fork만으로는 아무 일도 안 일어난다 — exec와 한 쌍이다

자식은 부모와 똑같은 프로그램입니다. 그럼 셸은 어떻게 다른 프로그램을 실행할까요. 복제한 다음 갈아탑니다. strace(system call trace, 시스템 콜 추적)로 sh -c 'ls /lab'을 들여다봤습니다.

$ strace -f -e trace=clone,execve,wait4 sh -c 'ls /lab'
execve("/usr/bin/sh", ["sh", "-c", "ls /lab"], ...) = 0
clone(child_stack=0xffffcb1108d0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD) = 4147
[pid  4147] execve("/usr/bin/ls", ["ls", "/lab"], ...) = 0
[pid  4146] wait4(-1, ...) = 4147

clone → 자식에서 execve(execute, 실행) → 부모는 wait4. 이 세 줄이 유닉스에서 프로그램이 실행되는 방식 그 자체입니다. fork는 갈래를 만들고, exec(execute)는 그 갈래의 내용물을 통째로 갈아치웁니다. 갈아치우는 것이므로 exec는 성공하면 리턴하지 않습니다.

여기서 예상 못 한 걸 하나 봤습니다. sh(dash, Debian Almquist SHell)가 부른 clone의 플래그가 CLONE_VM|CLONE_VFORK입니다. 일반 fork가 아니라 vfork(virtual memory fork, 주소 공간을 복제하지 않는 fork) 입니다. 어차피 곧바로 exec로 갈아탈 자식이라, 주소 공간을 복제할 필요조차 없다고 판단한 것입니다. 위 표에서 본 페이지 테이블 복사 비용을 통째로 아끼는 셈입니다.

그래서 bash(Bourne Again SHell)와 비교해 봤습니다.

--- dash ---
clone(child_stack=0xffffc6c56ce0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD)   ← vfork

--- bash ---
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)   ← 평범한 fork

같은 sh -c '/bin/true; /bin/true'인데 두 셸의 선택이 다릅니다. 그리고 bash 쪽 추적에서는 clone이 한 번인데 execve는 두 번 찍혔습니다. 마지막 명령에서는 bash가 복제를 아예 건너뛰고 자기 자신을 그 명령으로 갈아치웁니다. 어차피 그다음에 할 일이 없으니 갈래를 만들 이유가 없다는 판단입니다.

fork를 "무조건 일어나는 일"로 알고 있었는데, 실제 셸은 상황에 따라 vfork로 낮추거나 아예 생략하고 있었습니다.

복제되는 것에는 생각보다 많은 게 포함된다

마지막으로 짧은 프로그램 하나입니다. printf 한 줄 하고, 아무것도 안 하는 자식을 fork하고 끝냅니다.

printf("이 줄은 fork 전에 printf 했습니다\n");
if (fork() == 0) { return 0; }   // 자식은 즉시 종료
wait(NULL);

출력을 파이프로 넘기면 이렇게 나옵니다.

$ ./buf | cat
이 줄은 fork 전에 printf 했습니다
이 줄은 fork 전에 printf 했습니다

한 번 쓴 줄이 두 번 나왔습니다. 출력이 터미널이 아니면 stdio(standard input/output, C 표준 입출력 라이브러리)가 블록 버퍼링을 하기 때문에, printf 시점에 그 문자열은 아직 프로세스 메모리 안의 버퍼에 있습니다. fork는 그 버퍼까지 복제하고, 부모와 자식이 종료하면서 각자 자기 버퍼를 비웁니다.

fork가 복제하는 것은 "프로그램"이 아니라 그 시점의 프로세스 상태 전부라는 걸 이보다 짧게 보여주는 예를 저는 아직 못 봤습니다. 그리고 이건 로그가 이상하게 두 번 찍히는 버그의 실제 원인이기도 합니다.

프로세스와 스레드가 커널 입장에서 어떻게 같은 clone()의 다른 플래그인지, 무엇이 격리되고 무엇이 공유되는지는 프로세스와 스레드, 그리고 뮤텍스 에서 플래그 단위로 따로 정리했습니다. 이 글은 그 아래층(무엇이 복제되나)이 아니라 그다음(복제된 뒤 무슨 관계가 되나) 을 봅니다.


2. 층 ② 저장소 — 복제보다 중요한 건 남는 연결선

깃허브의 Fork 버튼은 프로세스 fork와 이름만 같은 게 아니라, 갈라진 뒤 원본과의 관계가 기록으로 남는다는 점에서 오히려 더 흥미롭습니다.

제가 2021년에 포크해 둔 저장소가 하나 있어서, 깃허브 REST(Representational State Transfer) API(Application Programming Interface)로 그 메타데이터를 그대로 뽑아 봤습니다.

$ gh api repos/Jseongwon/p2p-file-distributor
{
  "full_name":  "Jseongwon/p2p-file-distributor",
  "fork":       true,
  "parent":     "aliartiza75/p2p-file-distributor",
  "source":     "aliartiza75/p2p-file-distributor",
  "created_at": "2021-04-06T07:50:19Z"
}

git clone에는 이런 게 없습니다. 클론한 저장소가 어디서 왔는지는 제 로컬 .git/config의 remote 설정에만 적혀 있고, 그건 제가 지우면 사라집니다. 반면 포크는 서버 쪽에 관계가 박혀 있습니다.

parent와 source가 따로 있는 것도 이유가 있습니다. 포크의 포크를 뜨면 parent는 바로 위 저장소, source는 사슬의 맨 끝 원본을 가리킵니다. 제 경우는 원본을 직접 포크했으니 둘이 같습니다.

이 연결선이 있어서 가능한 일들이 있습니다.

  • PR(Pull Request) — 내 갈래의 커밋을 원본에 반영해 달라고 제안할 수 있습니다. 관계가 기록돼 있으니 깃허브가 두 저장소의 브랜치를 나란히 비교할 수 있습니다.
  • 즉시 완료되는 복제 — 포크 버튼은 수 GB 저장소도 거의 즉시 끝납니다. 같은 fork network 안의 저장소들이 객체 저장소를 공유하기 때문입니다. 앞에서 본 CoW와 발상이 같습니다. 다르게 만든 것만 내 것이 됩니다.

그래서 clone과 fork를 언제 헷갈렸나

제가 실제로 헷갈렸던 건 "포크를 떴는데 원본의 새 커밋이 자동으로 안 따라온다"는 지점이었습니다. 포크는 떠 온 시점의 스냅샷이고, 그 뒤 원본이 나아가는 것은 자동으로 반영되지 않습니다. 동기화하려면 원본을 upstream으로 직접 걸어야 합니다.

$ git remote add upstream https://github.com/aliartiza75/p2p-file-distributor.git
$ git fetch upstream && git merge upstream/master

여기서 프로세스 fork와의 대응이 선명해집니다. 프로세스 fork도 갈라진 순간 이후 부모의 메모리 변경은 자식에게 전달되지 않습니다. 두 층위 모두, 복제는 시점의 문제이고 그 뒤는 각자의 문제입니다.


3. 층 ③ 프로젝트 — 코드가 아니라 결정권이 갈라진다

세 번째 fork는 앞의 둘과 성격이 다릅니다. 복제는 쉬운 부분이고, 진짜로 갈라지는 것은 "무엇을 다음 버전에 넣을지 정하는 권한" 입니다.

Ubuntu ← Debian — 갈라진 뒤에도 계속 받아 오는 경우

제가 리눅스를 쓸 때는 거의 항상 데비안(Debian) 계열입니다. 그래서 가장 먼저 확인해 본 것도 이쪽이었습니다. 우분투(Ubuntu)는 2004년 데비안 unstable을 기반으로 갈라져 나왔는데, 20년이 지난 지금도 갈라진 관계가 패키지 메타데이터에 그대로 적혀 있습니다.

$ docker run --rm ubuntu:24.04 sh -c \
    "grep -E '^(ID|ID_LIKE)=' /etc/os-release; apt-cache show coreutils bash"
ID=ubuntu
ID_LIKE=debian
Package: coreutils
Maintainer:          Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Michael Stone <mstone@debian.org>
Version: 9.4-3ubuntu6.2
Package: bash
Maintainer:          Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Matthias Klose <doko@debian.org>
Version: 5.2.21-2ubuntu4

같은 패키지를 데비안 쪽에서 보면 Original-Maintainer 줄이 아예 없습니다. 데비안이 원본이니까요.

$ docker run --rm debian:bookworm-slim apt-cache show coreutils bash
Package: coreutils
Maintainer: Michael Stone <mstone@debian.org>      # Original- 접두어가 없다
Version: 9.1-1
Package: bash
Maintainer: Matthias Klose <doko@debian.org>
Version: 5.2.15-2+b13

두 가지가 눈에 띕니다.

  • Original-Maintainer: 우분투가 패키지를 가져오면서 관리자 필드를 자기 쪽으로 바꾸되, 데비안 쪽 관리자를 지우지 않고 별도 필드로 남겨 둡니다. 깃허브 fork의 parent/source 필드와 정확히 같은 성격의 기록입니다.
  • 버전 문자열 9.4-3ubuntu6.2: 앞의 9.4-3이 데비안 패키지 버전이고, 뒤의 ubuntu6.2가 그 위에 우분투가 올린 수정 횟수입니다. 원본 버전과 갈래 버전이 한 문자열 안에 같이 적혀 있는 셈입니다.

앞 장의 프로세스·저장소 fork와 달리, 이쪽은 갈라진 뒤에도 원본에서 계속 받아 옵니다. 우분투는 지금도 데비안에서 패키지를 가져와 그 위에 자기 수정을 얹는 방식으로 굴러갑니다. 같은 fork인데 동기화 정책이 "한 번 갈라지고 끝"이 아니라 "계속 당겨 온다" 쪽인 것입니다.

CentOS — 원본 쪽이 방향을 바꾼 경우

반대 방향의 사례도 있습니다. 제가 쓰는 계열은 아니지만, "포크한 쪽이 아니라 원본 쪽이 방향을 바꾸면 어떻게 되는가"를 보여 주는 예로 CentOS(Community ENTerprise Operating System)만 한 것이 없습니다.

CentOS는 RHEL(Red Hat Enterprise Linux) 소스를 재빌드한 배포판이었습니다. 그런데 2020년 12월 8일 발표로 성격이 바뀝니다.

"The future of the CentOS Project is CentOS Stream, and over the next year we’ll be shifting focus from CentOS Linux, the rebuild of Red Hat Enterprise Linux (RHEL), to CentOS Stream, which tracks just ahead of a current RHEL release."
— CentOS Project, 2020-12-08

같은 발표에 "CentOS Linux 8, as a rebuild of RHEL 8, will end at the end of 2021"이 함께 있었습니다. RHEL 뒤를 따라오던 것이 RHEL 앞을 달리는 것으로 바뀐 겁니다. 그리고 이 발표 이후 Rocky Linux, AlmaLinux 같은 새 갈래가 생겼습니다. 포크가 또 다른 포크를 낳는 과정을 실시간으로 본 사례입니다.

io.js — 갈래가 원본을 흡수한 경우

더 재미있는 건 Node.js입니다. 2014년, 릴리스 속도와 거버넌스에 대한 이견으로 io.js가 갈라져 나왔고, 2015년 9월 14일 다시 합쳐집니다.

"A record number of individuals and companies helped to contribute to the release, which combines both the Node.js project and io.js project in a single codebase under the direction of the Node.js Foundation."
— Node.js v4.0.0 릴리스 발표, 2015-09-14

여기서 저장소 메타데이터를 확인해 보면 흔적이 남아 있습니다.

$ gh api repos/nodejs/node        --jq '{full_name, created_at}'
{"full_name": "nodejs/node", "created_at": "2014-11-26T19:57:11Z"}

$ gh api repos/joyent/node        --jq '{full_name, created_at, archived}'
{"full_name": "nodejs/node-v0.x-archive", "created_at": "2009-05-27T16:29:46Z", "archived": true}

지금 우리가 npm install할 때 그 뒤에 있는 nodejs/node 저장소의 생성일은 2014년 11월입니다. 2009년에 시작된 원래 저장소는 node-v0.x-archive라는 이름으로 아카이브돼 있습니다. 그리고 nodejs/node에는 v1.0.0 태그가 남아 있습니다 — Node.js가 v0.12에서 v4로 건너뛴 이유가 여기 있습니다. 중간의 v1~v3은 io.js의 버전이었습니다.

갈래가 본류가 된 것입니다. 포크는 되돌릴 수 없는 결별이 아니라, 협상이 다시 열리면 합쳐질 수도 있는 상태라는 걸 이 사례가 보여줍니다.

세 사례를 나란히

사례 갈라진 이유 원본과의 관계 결말
Ubuntu ← Debian (2004) 릴리스 주기·사용성 정책 차이 패키지를 계속 가져옴(공생) — Original-Maintainer·-Nubuntu 버전에 흔적 둘 다 살아 있음
LibreOffice ← OpenOffice.org (2010) 인수 이후 거버넌스 불신 사실상 단절 갈래 쪽이 주류가 됨
io.js ← Node.js (2014) 릴리스 속도·거버넌스 15개월 만에 재통합 갈래가 본류를 대체

세 줄의 공통점은 기술적 차이가 원인인 사례가 하나도 없다는 것입니다. 코드는 어차피 복제되니까요. 갈라지는 것은 언제나 누가 결정하는가입니다.


4. 세 층위를 한 표에

프로세스 fork(2) 저장소 fork(GitHub) 프로젝트 fork
복제 단위 주소 공간·fd(file descriptor) 테이블·시그널 설정 등 프로세스 상태 전부 저장소 전체(객체는 서버에서 공유) 소스 전체 + 커뮤니티 일부
복제 시점 호출 순간 버튼 누른 순간 선언한 순간
실제 복사량 쓰는 만큼만(CoW) — 자식 Private_Dirty 36kB에서 시작 다르게 만든 만큼만 처음엔 0(코드는 같다)
갈라진 뒤 원본 변경 전달 안 됨 자동 전달 안 됨(upstream 수동) 전달 안 됨(체리픽은 수동)
원본과의 연결 ppid(Parent Process ID — 부모 죽으면 재부모화) parent/source 필드로 서버에 기록 사회적 관계·라이선스
되돌리기 자식 종료 PR(Pull Request)로 병합 재통합 가능(io.js)
비용 1GB 기준 9.9ms 거의 0 사람과 시간

5. 정리

  • fork의 어원은 복제가 아니라 갈래(furca)입니다. 세 층위 모두 흥미로운 부분은 복제되는 순간이 아니라 갈라진 다음의 관계에 있습니다.
  • fork(2)는 한 번 부르고 두 번 리턴합니다. 자식은 0, 부모는 자식 PID. 반환값이 코드의 갈래를 가릅니다.
  • CoW는 "복사 안 함"이 아니라 "쓰는 만큼만" 입니다. 256MB를 fork한 자식의 Private_Dirty는 36kB였고, 절반을 쓰자 정확히 절반이 넘어왔습니다.
  • 그래도 페이지 테이블은 복사합니다. 16MB에서 0.43ms, 1GB에서 9.88ms 로 늘었습니다.
  • fork는 exec와 한 쌍입니다. 그리고 실제 셸은 이걸 최적화합니다. dash는 vfork를 쓰고, bash는 마지막 명령에서 복제를 아예 생략합니다.
  • 복제되는 것에는 stdio 버퍼도 포함됩니다. 파이프로 넘기면 같은 줄이 두 번 찍힙니다.
  • 깃허브 fork는 parent/source라는 서버 쪽 관계 기록이 clone과의 진짜 차이입니다.
  • 프로젝트 포크가 갈라놓는 것은 코드가 아니라 결정권입니다. 그래서 결정권 문제가 풀리면 io.js처럼 다시 합쳐지기도 합니다.

같은 단어를 세 번 다르게 쓴 게 헷갈림의 원인인 줄 알았는데, 재보고 나니 셋 다 정확히 같은 것을 말하고 있었습니다. "여기서부터 따로 간다"는 선언이요. 다만 따로 가는 방식이 커널·서버·커뮤니티에서 각각 달랐을 뿐입니다.

다음 편에서는 이 fork처럼 이름 자체가 설명인 명령어들을 봅니다. chmod가 왜 chmod인지 풀어 보면, 유닉스 명령어가 외울 대상이 아니라 읽을 대상이었다는 게 드러납니다.

함께 읽으면 좋은 글:


참고 (1차 출처)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다