Chương 6 · Tiến trình Chapter 6 · Processes
Một tiến trình là gì, và nó nằm ra sao trong bộ nhớ Tiến trình là gì, và nó chiếm chỗ nào trong bộ nhớ What a process is, and how it is laid out in memory
Kerrisk định nghĩa tiến trình (process) là một phiên bản đang chạy của một chương trình, phân biệt rõ với chương trình (program) — vốn chỉ là một tệp mô tả cách dựng nên tiến trình.1 Chương đi qua: số định danh tiến trình (getpid()) và của tiến trình cha (getppid()); cách bộ nhớ của tiến trình được chia thành các segment (text, data, BSS, heap, stack); cơ chế bộ nhớ ảo và khung ngăn xếp (stack frame); đối số dòng lệnh (argc/argv); danh sách môi trường (environ); và cuối cùng là lối nhảy phi cục bộ setjmp()/longjmp().1
Chương trình là một tệp nằm yên trên đĩa, như một công thức nấu ăn. Tiến trình là lúc công thức đó đang được thực sự nấu: có bếp, có nồi, có nguyên liệu. Chương này mổ xẻ cái "lúc đang nấu" ấy — nó tên gì (số định danh), chiếm những ngăn nhớ nào, lấy đối số và biến môi trường từ đâu. Nhớ nhé: chương trình là tệp nằm im, tiến trình là tệp đó lúc đang chạy.
Kerrisk defines a process as an instance of an executing program, carefully distinguished from a program — which is merely a file describing how to construct that process.1 The chapter walks through: the process ID (getpid()) and parent process ID (getppid()); how a process's memory is divided into segments (text, data, BSS, heap, stack); the virtual memory scheme and stack frames; command-line arguments (argc/argv); the environment list (environ); and finally the nonlocal goto via setjmp()/longjmp().1
Góc nhìn của nhân: một tiến trình gồm bộ nhớ không gian người dùng chứa mã và biến, cộng với một loạt cấu trúc dữ liệu trong nhân lưu trạng thái của nó — các số định danh, bảng bộ nhớ ảo, bảng bộ mô tả tệp đang mở, thông tin tín hiệu, giới hạn tài nguyên, thư mục làm việc hiện tại, v.v. Chính vì vậy một chương trình (tệp) có thể sinh ra nhiều tiến trình, và nhiều tiến trình có thể cùng chạy một chương trình.1The kernel's view: a process consists of user-space memory holding program code and variables, plus a range of kernel data structures recording its state — identifier numbers, virtual memory tables, the table of open file descriptors, signal information, resource limits, the current working directory, and more. That is why one program (a file) can spawn many processes, and many processes can run the same program.1
Luận đề của chương: một tiến trình là một thực thể trừu tượng do nhân định nghĩa để chạy một chương trình; hiểu được danh tính của nó (PID/PPID), bản đồ bộ nhớ của nó (các segment trên nền bộ nhớ ảo), cách nó nhận đối số và môi trường, cùng những lối điều khiển đặc biệt như longjmp() — là nắm được "giải phẫu" của mọi chương trình đang chạy trên Linux.1
Ý chính: tiến trình là chương trình lúc đang sống. Biết nó tên gì, chiếm những ngăn nhớ nào, nhận đầu vào từ đâu — là hiểu được bộ khung của mọi phần mềm đang chạy. Nhớ nhé: hiểu một tiến trình là hiểu một chương trình lúc nó đang thở.
The chapter's thesis: a process is an abstract entity defined by the kernel to run a program; grasping its identity (PID/PPID), its memory map (the segments over a virtual-memory substrate), how it receives arguments and an environment, and special control transfers such as longjmp() — is grasping the "anatomy" of every running program on Linux.1
Cụm khái niệm 01 Concept cluster 01
Chương trình là một tệp, tiến trình là tệp đó lúc chạy — và mỗi tiến trình có một danh tính Chương trình là tệp nằm im; tiến trình là nó lúc đang chạy, có số định danh riêng A program is a file, a process is that file running — and each process has an identity
Một chương trình là một tệp chứa thông tin mô tả cách dựng nên một tiến trình lúc chạy: định dạng tệp thực thi, mã máy, địa chỉ điểm vào, dữ liệu khởi tạo, bảng ký hiệu và thông tin liên kết động.1 Hầu hết hệ UNIX ngày nay (gồm Linux) dùng định dạng ELF (Executable and Linking Format), thay cho hai định dạng cũ hơn là a.out và COFF.14 Một tiến trình là một phiên bản đang chạy của chương trình đó — nói chặt hơn, là một thực thể trừu tượng do nhân định nghĩa và được cấp tài nguyên để chạy chương trình.1
Hãy hình dung một bản nhạc viết trên giấy — đó là chương trình. Khi dàn nhạc chơi bản nhạc ấy thì mới thành buổi hòa nhạc — đó là tiến trình. Một bản nhạc có thể được chơi nhiều lần, nhiều nơi cùng lúc; cũng vậy, một chương trình có thể sinh ra nhiều tiến trình. Nhớ nhé: bản nhạc là tệp, buổi diễn là tiến trình.
A program is a file containing the information needed to construct a process at run time: the executable file format, machine-language instructions, the entry-point address, initialization data, symbol tables, and dynamic-linking information.1 Most UNIX systems today (including Linux) use the ELF format (Executable and Linking Format), replacing the older a.out and COFF formats.14 A process is a running instance of that program — more precisely, an abstract entity defined by the kernel and allocated resources to run the program.1
Định nghĩa. Mỗi tiến trình có một process ID (PID) — một số nguyên dương định danh duy nhất tiến trình đó trên hệ thống. getpid() trả về PID của tiến trình gọi và luôn thành công. Kiểu trả về là pid_t, một kiểu nguyên do SUSv3 quy định để lưu PID.12
Nói đơn giản. Mỗi tiến trình được cấp một con số riêng, như số báo danh. Gọi getpid() là biết số của chính mình. Nhớ nhé: PID là số báo danh của một tiến trình.
Definition. Each process has a process ID (PID) — a positive integer that uniquely identifies it on the system. getpid() returns the calling process's PID and always succeeds. Its return type is pid_t, an integer type SUSv3 specifies for storing process IDs.12
#include <unistd.h>
pid_t getpid(void); /* số của tôi */
pid_t getppid(void); /* số của cha tôi */
/* cả hai luôn thành công */
getpid() --> số của tôi
getppid() --> số của cha tôi
(không bao giờ hỏng)
#include <unistd.h>
pid_t getpid(void); /* my PID */
pid_t getppid(void); /* my parent's PID */
/* both always succeed */
-
PID limit
PID không vô hạn — nó quay vòng Số định danh có trần, dùng hết thì quay lại PIDs are finite — they wrap around
Nhân Linux giới hạn PID ở mức ≤ 32.767. Khi tạo tiến trình mới, nhân cấp PID kế tiếp theo thứ tự; mỗi lần chạm trần, bộ đếm được đặt lại — nhưng quay về 300 chứ không phải 1, vì nhiều PID thấp đã bị các tiến trình hệ thống và daemon chiếm giữ cố định.1
Số định danh không tăng mãi mãi; tới một mức thì bắt đầu đếm lại. Nhưng không quay về 1, mà về khoảng 300, để tránh dẫm lên các số mà tiến trình hệ thống đang giữ. Nhớ nhé: PID có trần, và khi quay vòng nó bỏ qua những số thấp.
The Linux kernel limits PIDs to ≤ 32,767. When a new process is created, it gets the next sequentially available PID; each time the limit is reached, the counter is reset — but to 300, not 1, because many low-numbered PIDs are permanently in use by system processes and daemons.1
Bạn gặp nó ở đâuWhere you meet it Khi dùng PID làm phần của tên tệp tạm "duy nhất" — nhớ rằng theo thời gian PID sẽ được tái sử dụng, nên không duy nhất vĩnh viễn.1 Khi lấy PID ghép vào tên tệp tạm — nhớ rằng con số đó sẽ có ngày bị dùng lại. When using a PID as part of a "unique" temporary filename — remember PIDs are reused over time, so they are not unique forever.1Chi tiết của tác giảThe author's detail
Trên Linux 2.4 trở về trước, trần 32.767 do hằng nhân
PID_MAXđịnh. Với Linux 2.6, trần mặc định vẫn là 32.767 nhưng chỉnh được qua tệp/proc/sys/kernel/pid_max(giá trị trong tệp lớn hơn PID tối đa 1). Trên nền 32-bit tối đa là 32.768; trên nền 64-bit có thể chỉnh tới 2²² (xấp xỉ 4 triệu).13On Linux 2.4 and earlier the 32,767 limit was set by the kernel constantPID_MAX. With Linux 2.6 the default stays 32,767 but is adjustable via/proc/sys/kernel/pid_max(whose value is one greater than the maximum PID). On 32-bit platforms the maximum is 32,768; on 64-bit it can be raised to 2²² (about 4 million).13Vì sao điều này quan trọngWhy it matters
Trần PID là lý do không nên giả định PID là định danh duy nhất lâu dài; để an toàn tuyệt đối, các cơ chế hiện đại dùng thêm thông tin (ví dụ thời điểm khởi tạo) chứ không chỉ con số PID.1The PID limit is why you should not assume a PID is a durable unique identifier; for strict safety, modern mechanisms combine extra information (such as a start time) rather than trusting the PID number alone.1
-
getppid()
Mỗi tiến trình có cha — và cây gia phả trỏ về init Mỗi tiến trình có một cha, truy ngược về tiến trình gốc Every process has a parent — the tree traces back to init
Mỗi tiến trình có một cha — tiến trình đã tạo ra nó — và biết PID của cha qua
getppid(). Thuộc tính PPID này dựng nên quan hệ hình cây của mọi tiến trình: cha của mỗi tiến trình lại có cha của nó, truy ngược tới tiến trình 1,init, tổ tiên của tất cả.12 Có thể xem "cây gia phả" này bằng lệnhpstree(1).1Tiến trình nào cũng do một tiến trình khác sinh ra — đó là cha của nó. Lần theo cha của cha, cuối cùng ta về tới tiến trình đầu tiên tên
init(số 1), ông tổ của mọi tiến trình. Nhớ nhé: mọi tiến trình đều có gốc gác, truy ngược về init số 1.Every process has a parent — the process that created it — and learns its parent's PID via
getppid(). This PPID attribute forms the tree-like relationship of all processes: each process's parent has its own parent, back to process 1,init, the ancestor of all.12 You can view this "family tree" withpstree(1).1Bạn gặp nó ở đâuWhere you meet it Khi một tiến trình con "mồ côi" vì cha ruột kết thúc trước — nó đượcinitnhận nuôi, và từ đógetppid()trong con trả về 1.1 Khi cha kết thúc trước con, con được init nhận nuôi và từ đó "cha" của nó là số 1. When a child is "orphaned" because its real parent ends first — it is adopted byinit, and thereaftergetppid()in the child returns 1.1Tra ở đâu cho chắcWhere to look it up
Trường
PPidtrong tệp Linux/proc/PID/statuscho biết cha của bất kỳ tiến trình nào — nên khi nghi ngờ quan hệ cha–con, đó là nơi tra.13ThePPidfield in the Linux-specific/proc/PID/statusfile reveals any process's parent — so when a parent–child relationship is in doubt, that is where to look.13Một ngoại lệOne exception
Trừ một ít tiến trình hệ thống như
init(PID 1), không có quan hệ cố định nào giữa một chương trình và PID của tiến trình được tạo để chạy nó — PID được cấp động lúc chạy.1With the exception of a few system processes such asinit(PID 1), there is no fixed relationship between a program and the PID of the process created to run it — PIDs are assigned dynamically at run time.1 -
never fails
Hai lời gọi không bao giờ hỏng Hai hàm này không bao giờ thất bại Two calls that never fail
Cả
getpid()lẫngetppid()đều luôn thành công: chúng không có trường hợp lỗi và không đặterrno. Đây là một trong số rất ít system call mà không cần kiểm tra giá trị trả về — một ngoại lệ hiếm so với quy tắc "luôn kiểm tra kết quả" đã học ở Chương 3.12Hai hàm hỏi "số của tôi" và "số của cha tôi" thì không thể hỏng — câu trả lời luôn có. Nên với chúng, không cần bọc kiểm tra lỗi. Nhớ nhé: đây là ngoại lệ hiếm — hầu hết lời gọi khác vẫn phải kiểm tra.
Both
getpid()andgetppid()always succeed: they have no error case and set noerrno. They are among the very few system calls whose return value need not be checked — a rare exception to the "always check the result" rule from Chapter 3.12Bạn gặp nó ở đâuWhere you meet it Khi đọc mã thấygetpid()gọi trần không bọcif— đó là đúng, không phải cẩu thả. Thấy ai gọigetpid()mà không kiểm tra lỗi — yên tâm, vậy là đúng. When you read code that callsgetpid()bare, without anif— that is correct, not careless.
Cụm khái niệm 02 Concept cluster 02
Bộ nhớ của tiến trình chia thành các segment — trên nền bộ nhớ ảo Bộ nhớ tiến trình chia thành những ngăn có vai trò riêng A process's memory divides into segments — over a virtual-memory substrate
Bộ nhớ cấp cho mỗi tiến trình gồm nhiều phần, thường gọi là segment: text (mã máy, để chỉ-đọc và dùng chung), initialized data (biến toàn cục/tĩnh có khởi tạo tường minh), uninitialized data hay BSS (biến toàn cục/tĩnh không khởi tạo, được đặt về 0), stack (ngăn xếp các khung hàm), và heap (vùng cấp phát động lúc chạy, có đỉnh gọi là program break).1 Lệnh size(1) in ra kích thước các segment text, data và bss của một tệp thực thi.15
Hãy coi bộ nhớ của một tiến trình như một ngôi nhà nhiều phòng: phòng mã giữ các lệnh (khóa lại, chỉ được đọc), phòng dữ liệu giữ các biến, phòng ngăn xếp cho các việc đang làm dở, và phòng đống (heap) để lấy thêm chỗ khi cần. Mỗi phòng một việc. Nhớ nhé: bộ nhớ tiến trình là ngôi nhà nhiều phòng, mỗi phòng một nhiệm vụ.
The memory allocated to each process has several parts, usually called segments: the text segment (machine code, read-only and sharable), the initialized data segment (global/static variables explicitly initialized), the uninitialized data or BSS segment (global/static variables not initialized, set to 0), the stack (a stack of function frames), and the heap (an area for run-time dynamic allocation whose top is the program break).1 The size(1) command prints the sizes of a binary's text, data, and bss segments.15
Định nghĩa. BSS là tên lịch sử của segment dữ liệu không khởi tạo, bắt nguồn từ một ký hiệu assembler cũ nghĩa là "block started by symbol". Đặt biến chưa khởi tạo riêng ra có một lợi ích thiết thực: tệp thực thi không cần dành chỗ trên đĩa cho chúng — chỉ cần ghi lại vị trí và kích thước, rồi bộ nạp cấp phát và đặt về 0 lúc chạy.1
Nói đơn giản. Biến chưa gán giá trị không cần lưu gì trên đĩa — chỉ cần ghi "để trống chừng này chỗ". Khi chạy, hệ thống mới dọn chỗ và đặt tất cả về 0. Nhớ nhé: biến chưa khởi tạo không tốn chỗ trên đĩa.
Definition. BSS is the historical name for the uninitialized data segment, from an old assembler mnemonic for "block started by symbol." Keeping uninitialized variables separate has a practical payoff: the executable need not store space for them on disk — it merely records their location and size, and the loader allocates and zeroes them at run time.1
cao ┌───────────┐ argv, environ
│ stack │ ↓ mọc xuống
│ ... │
│ heap │ ↑ break
│ uninit (bss)│
│ init data │
thấp └ text (ro) ┘
cao │ stack (ngăn xếp) │ ↓
│ heap (đống) │ ↑
│ dữ liệu │
thấp│ mã (chỉ đọc) │
high ┌───────────┐ argv, environ
│ stack │ ↓ grows down
│ ... │
│ heap │ ↑ break
│ uninit (bss)│
│ init data │
low └ text (ro) ┘
-
text / data / bss
Vì sao biến lại nằm ở ba chỗ khác nhau Ba kiểu biến, ba chỗ ở khác nhau Why variables live in three different places
Segment text được đặt chỉ-đọc để một tiến trình không vô tình sửa lệnh của chính nó qua con trỏ hỏng, và được chia sẻ để nhiều tiến trình cùng chạy một chương trình chỉ cần một bản mã trong bộ nhớ.1 Biến toàn cục/tĩnh có khởi tạo nằm ở initialized data (đọc từ tệp lúc nạp); biến toàn cục/tĩnh không khởi tạo nằm ở BSS (đặt về 0 lúc chạy). Listing 6-1 trong sách minh họa từng loại biến C rơi vào segment nào.1
Mã lệnh được "khóa" lại (chỉ đọc) để không tự phá chính mình, và dùng chung cho nhiều tiến trình. Biến đã có giá trị thì cất ở ngăn dữ liệu; biến chưa có giá trị thì để ở ngăn BSS, hệ thống tự đặt về 0. Nhớ nhé: mã thì khóa và dùng chung, biến thì tùy đã khởi tạo hay chưa mà ở ngăn khác nhau.
The text segment is made read-only so a process cannot accidentally modify its own instructions via a bad pointer, and sharable so many processes running the same program need only one copy of the code in memory.1 Global/static variables that are initialized go in initialized data (read from the file at load time); those that are not go in BSS (zeroed at run time). The book's Listing 6-1 shows which segment each kind of C variable falls into.1
Bạn gặp nó ở đâuWhere you meet it Khi chạysizetrên một tệp nhị phân và thấy ba cột text, data, bss — đó chính là ba segment này.5 Khi gõsize a.outvà thấy ba cột text, data, bss — đó là ba ngăn vừa nói. When you runsizeon a binary and see the three columns text, data, bss — those are exactly these segments.5Đừng nhầm thuật ngữA terminology caveat
Từ "segment" ở đây là phân chia logic bộ nhớ ảo của tiến trình trên hệ UNIX, không phải cơ chế phân đoạn phần cứng (hardware segmentation) trên x86-32. Trong đặc tả ELF, người ta còn dùng từ section.14The word "segment" here means a logical division of a process's virtual memory on UNIX, not the hardware segmentation of x86-32. In the ELF specification the term section is also used.14
Hệ quả: bộ nhớ tĩnhConsequence: static memory
Khi sách nói một hàm thư viện "trả về con trỏ tới bộ nhớ cấp phát tĩnh", nghĩa là bộ nhớ nằm ở segment data/BSS; nó tồn tại độc lập với lời gọi và có thể bị lời gọi sau ghi đè — làm hàm trở nên không khả tái nhập (nonreentrant).1When the book says a library function "returns a pointer to statically allocated memory," that memory lives in the data/BSS segment; it exists independently of the call and may be overwritten by later calls — rendering the function nonreentrant.1
-
virtual memory
Bộ nhớ ảo: chỉ phần đang dùng mới nằm trong RAM Bộ nhớ ảo: không cần cả chương trình ở trong RAM cùng lúc Virtual memory: only the parts in use sit in RAM
Các segment này nằm trên một sơ đồ bộ nhớ ảo: bộ nhớ của chương trình được chia thành những đơn vị nhỏ bằng nhau gọi là trang (page). Tại một thời điểm, chỉ một tập con các trang cần có mặt trong các khung trang vật lý — gọi là resident set; các trang không dùng được giữ ở vùng swap. Khi truy cập một trang không có trong RAM, một page fault xảy ra và nhân nạp trang đó vào.1 Chương trình có thể hỏi kích thước trang qua
sysconf(_SC_PAGESIZE).16Máy không cần nhét cả chương trình vào RAM cùng lúc. Nó cắt bộ nhớ thành nhiều "trang" nhỏ, và chỉ giữ trong RAM những trang đang thật sự dùng. Cần tới trang chưa có thì máy đi lấy vào — như thủ thư chỉ bày ra bàn vài cuốn sách đang đọc. Nhớ nhé: chỉ phần đang dùng mới cần nằm trong RAM.
These segments sit on a virtual memory scheme: a program's memory is split into small, equal-sized units called pages. At any moment, only a subset of pages need be present in physical page frames — the resident set; unused pages are kept in the swap area. Accessing a page not in RAM causes a page fault, at which point the kernel loads it.1 A program can query the page size via
sysconf(_SC_PAGESIZE).16Bạn gặp nó ở đâuWhere you meet it Mỗi khi một tiến trình dùng được không gian địa chỉ lớn hơn RAM vật lý, hay khi nhiều tiến trình cùng chia sẻ một trang mã chỉ-đọc — đó là bộ nhớ ảo đang làm việc.1 Khi chương trình dùng được nhiều bộ nhớ hơn RAM thật có — đó là nhờ bộ nhớ ảo. Whenever a process uses an address space larger than physical RAM, or several processes share one read-only code page — that is virtual memory at work.1 -
stack frames
Ngăn xếp: mỗi lần gọi hàm là một khung Ngăn xếp: mỗi lời gọi hàm thêm một tầng The stack: one frame per function call
Segment stack co giãn động, chứa các khung ngăn xếp (stack frame) — mỗi hàm đang được gọi có đúng một khung, lưu biến cục bộ (biến automatic), đối số và giá trị trả về của hàm.1 Trên Linux/x86-32, ngăn xếp người dùng lớn lên theo hướng địa chỉ giảm dần; mỗi lời gọi hàm thêm một khung lên đỉnh, mỗi lần hàm trở về thì gỡ khung đó đi. Sách minh họa bằng Figure 6-3.1
Mỗi khi gọi một hàm, máy đặt lên ngăn xếp một "khay" riêng chứa các biến cục bộ và đối số của hàm đó. Hàm xong thì cất khay đi. Gọi hàm lồng nhau thì khay chồng lên khay, như chồng đĩa. Nhớ nhé: mỗi hàm đang chạy có một khay riêng trên ngăn xếp, xong là dọn.
The stack segment grows and shrinks dynamically, holding stack frames — each currently-called function has exactly one frame, storing its local (automatic) variables, arguments, and return value.1 On Linux/x86-32, the user stack grows toward decreasing addresses; each call pushes a frame on top, each return pops it. The book illustrates this in Figure 6-3.1
Bạn gặp nó ở đâuWhere you meet it Khi đệ quy quá sâu gây stack overflow, hoặc khi một con trỏ trỏ vào biến cục bộ của một hàm đã trở về trỏ vào vùng rác — đều là hệ quả của cơ chế khung ngăn xếp. Khi đệ quy quá sâu làm "tràn ngăn xếp" — đó là vì các khay chồng cao quá. When runaway recursion causes a stack overflow, or a pointer to a returned function's local variable points at garbage — both follow from how stack frames work.
Cụm khái niệm 03 Concept cluster 03
Đầu vào của tiến trình: đối số dòng lệnh và danh sách môi trường Tiến trình nhận gì khi khởi động: đối số và biến môi trường A process's inputs: command-line arguments and the environment
Mọi chương trình C vào qua hàm main(int argc, char *argv[]): argc là số đối số dòng lệnh, argv là mảng con trỏ tới các chuỗi kết thúc bằng null. Theo quy ước, argv[0] là tên chương trình, và danh sách kết thúc bằng một con trỏ NULL (tức argv[argc] là NULL).1 Song song, mỗi tiến trình mang một danh sách môi trường (environment list) — mảng các chuỗi dạng name=value — truy cập trong C qua biến toàn cục char **environ.17
Khi bạn gõ một lệnh kèm theo vài chữ phía sau, những chữ đó được trao cho chương trình qua argc (đếm số) và argv (danh sách). Ngoài ra, mỗi tiến trình còn mang theo một "túi hành lý" gồm các cặp tên=giá_trị gọi là biến môi trường. Nhớ nhé: argv là lời bạn gõ, environ là túi hành lý tiến trình mang theo.
Every C program enters through main(int argc, char *argv[]): argc is the number of command-line arguments, argv an array of pointers to null-terminated strings. By convention, argv[0] is the program's name, and the list is terminated by a NULL pointer (so argv[argc] is NULL).1 In parallel, each process carries an environment list — an array of name=value strings — reached in C via the global char **environ.17
Định nghĩa. Khi tạo tiến trình mới, nó thừa kế một bản sao danh sách môi trường của cha. Đây là một cơ chế một chiều để truyền thông tin từ cha sang con: sau khi con đã được tạo, cha và con mỗi bên tự thay đổi môi trường riêng mà không ảnh hưởng bên kia.17
Nói đơn giản. Con sinh ra được chép lại túi hành lý của cha. Nhưng từ đó mỗi người sửa túi của mình, không đụng tới túi người kia. Nhớ nhé: con chép túi của cha một lần, rồi ai đi đường nấy.
Definition. When a new process is created, it inherits a copy of its parent's environment. This is a one-way channel for passing information from parent to child: after the child is created, parent and child each change their own environment without affecting the other.17
extern char **environ;
char *p = getenv("SHELL"); /* đọc */
setenv("GREET","Hi",1); /* thêm/ghi đè */
putenv("K=V"); /* thêm */
unsetenv("GREET"); /* xóa một */
clearenv(); /* xóa sạch */
getenv("SHELL") --> đọc
setenv(...) --> thêm/ghi đè
unsetenv(...) --> xóa một
clearenv() --> xóa sạch
extern char **environ;
char *p = getenv("SHELL"); /* read */
setenv("GREET","Hi",1); /* add/overwrite */
putenv("K=V"); /* add */
unsetenv("GREET"); /* remove one */
clearenv(); /* wipe all */
-
argc / argv
argv[0] là tên chương trình, danh sách kết thúc bằng NULL Phần tử đầu là tên lệnh, phần tử cuối là NULL argv[0] is the program name, the list ends in NULL
Vì
argv[argc]luôn là NULL, ta có thể duyệt đối số bằng con trỏ thay vì chỉ số:for (p = argv; *p != NULL; p++). Việcargv[0]chứa tên dùng để gọi chương trình cho phép một tệp thực thi hành xử khác nhau tùy tên được gọi (một mẹo được dùng thật trong một số công cụ).1Vì phần tử cuối cùng luôn là NULL, chương trình biết khi nào hết đối số mà không cần đếm. Và vì phần tử đầu là tên lệnh, cùng một tệp có thể cư xử khác nhau tùy bạn gọi nó bằng tên gì. Nhớ nhé: NULL ở cuối là dấu hết, argv[0] là tên gọi.
Because
argv[argc]is always NULL, you can walk the arguments with a pointer rather than an index:for (p = argv; *p != NULL; p++). Thatargv[0]holds the name used to invoke the program lets one executable behave differently depending on the name it is called by (a trick genuinely used by some tools).1Bạn gặp nó ở đâuWhere you meet it Một hạn chế:argc/argvchỉ sẵn có trongmain(); muốn dùng ở hàm khác phải truyền chúng đi hoặc đặt một biến toàn cục trỏ tớiargv.1 Lưu ý: hai biến này chỉ có trong hàm main; muốn dùng chỗ khác phải truyền đi. A limitation:argc/argvare available only inmain(); to use them elsewhere you must pass them along or set a global pointing toargv.1Chúng nằm ở đâu trong bộ nhớWhere they live in memory
Như Figure 6-1 cho thấy, các mảng
argv,environvà các chuỗi chúng trỏ tới nằm ở một vùng gần đỉnh ngăn xếp.1As Figure 6-1 shows, theargvandenvironarrays and the strings they point to sit in a region near the top of the stack.1Có giới hạn kích thướcThere is a size limit
Tổng không gian cho
argvvàenvironbị giới hạn; từ nhân 2.6.23, giới hạn này được tính bằng một phần tư giới hạn mềm của kích thước ngăn xếp (RLIMIT_STACK).1The total space forargvandenvironis limited; since kernel 2.6.23 this limit is computed as one-quarter of the soft stack-size limit (RLIMIT_STACK).1 -
getenv / setenv
Đọc và sửa môi trường — và một bẫy con trỏ Đọc và sửa biến môi trường cho đúng cách Reading and modifying the environment — and a pointer trap
getenv(name)trả về con trỏ tới chuỗi giá trị ứng với biến, hoặc NULL nếu không có. SUSv3 cho phépgetenv()trả về kết quả trong một vùng đệm tĩnh có thể bị ghi đè bởi lời gọi sau tớigetenv(),setenv(),putenv()hayunsetenv()— nên nếu cần giữ chuỗi đó, hãy sao chép nó ra chỗ khác.18 Ngoài ra, chương trình không nên sửa chuỗi màgetenv()trả về, vì (ở phần lớn hiện thực) nó chính là một phần của môi trường.1getenv("TÊN")cho bạn giá trị của một biến môi trường. Nhưng đừng sửa trực tiếp chuỗi nhận được, và nếu muốn giữ nó lâu thì hãy chép ra nơi của riêng mình, vì chỗ gốc có thể bị ghi đè. Nhớ nhé: đọc bằng getenv thì chép ra, đừng sửa tại chỗ.getenv(name)returns a pointer to the value string for a variable, or NULL if none exists. SUSv3 permitsgetenv()to return its result in a static buffer that may be overwritten by later calls togetenv(),setenv(),putenv(), orunsetenv()— so if you need to keep the string, copy it elsewhere.18 Also, a program should not modify the stringgetenv()returns, since (in most implementations) it is part of the environment itself.1Bạn gặp nó ở đâuWhere you meet it Shell chính là nơi quen thuộc: đặt một biến rồiexportđể nó được truyền sang mọi tiến trình con mà shell tạo ra.1 Trong shell: đặt biến rồiexportlà nó theo sang mọi lệnh con bạn chạy sau đó. The shell is the familiar place: set a variable andexportit so it is passed to every child process the shell creates.1setenv vs putenvsetenv vs putenv
setenv()sao chép đối số của nó vào một vùng đệm mới, nên sửa đổi chuỗi sau khi gọi không gây vấn đề. Ngược lại,putenv()làm chuỗi bạn đưa trở thành một phần của môi trường — sửa nó về sau sẽ sửa luôn môi trường. Lưu ýputenv()trả về giá trị khác 0 khi lỗi, chứ không phải −1.18setenv()copies its arguments into a new buffer, so changing the string afterward is harmless. By contrast,putenv()makes the string you pass become part of the environment — changing it later changes the environment too. Noteputenv()returns a nonzero value on error, not −1.18Nguồn gốc và tính khả chuyểnOrigins and portability
Cả
setenv()vàunsetenv()bắt nguồn từ BSD và ít phổ biến hơnputenv(); chúng không có trong POSIX.1 gốc hay SUSv2 (nhưng có trong SUSv3). Trước glibc 2.2.2,unsetenv()được khai báo trả vềvoid.18Bothsetenv()andunsetenv()derive from BSD and are less widespread thanputenv(); they are absent from the original POSIX.1 and SUSv2 (but present in SUSv3). Before glibc 2.2.2,unsetenv()was prototyped as returningvoid.18 -
clearenv
Xóa sạch môi trường — và một rò rỉ bộ nhớ tinh vi Dọn sạch môi trường, nhưng coi chừng rò rỉ Wiping the environment — and a subtle leak
Đôi khi cần xóa toàn bộ môi trường rồi dựng lại (ví dụ vì lý do bảo mật trong chương trình set-user-ID). Hàm thư viện
clearenv()làm đúng việc đó — về bản chất là gánenviron = NULL. Nhưng kết hợpclearenv()vớisetenv()có thể gây rò rỉ bộ nhớ:setenv()cấp phát một vùng đệm rồi đưa vào môi trường, cònclearenv()không giải phóng vùng đệm ấy.19Muốn làm sạch hoàn toàn túi hành lý rồi xếp lại từ đầu, dùng
clearenv(). Nhưng cẩn thận: nếu trước đó đã dùngsetenv()để thêm đồ, lệnh dọn này không thu hồi chỗ nhớ đã mượn — có thể để lại "rác". Nhớ nhé: clearenv dọn danh sách nhưng không nhất thiết trả lại bộ nhớ.Sometimes you must erase the whole environment and rebuild it (for example, for security in a set-user-ID program). The
clearenv()library function does exactly that — essentially settingenviron = NULL. But combiningclearenv()withsetenv()can cause a memory leak:setenv()allocates a buffer and makes it part of the environment, andclearenv()does not free that buffer.19Bạn gặp nó ở đâuWhere you meet it Trong chương trình đặc quyền cần một môi trường "sạch" đã biết trước — dọn sạch rồi chỉ thêm đúng các biến mình kiểm soát, thay vì tin vào môi trường thừa kế.1 Trong chương trình nhạy cảm cần môi trường sạch: dọn hết rồi chỉ thêm đúng thứ mình muốn. In a privileged program needing a known-clean environment — wipe it, then add only the variables you control, rather than trusting the inherited one.1Một lưu ý về chuẩnA standards note
Nhiều hệ UNIX có
clearenv()nhưng nó không được SUSv3 quy định; trên glibc, cần định nghĩa_GNU_SOURCEhay_BSD_SOURCEđể lấy khai báo của nó.19Many UNIX systems provideclearenv(), but it is not specified by SUSv3; on glibc you must define_GNU_SOURCEor_BSD_SOURCEto get its declaration.19Khi environ bằng NULLWhen environ is NULL
Sau
clearenv(),environlà NULL; nhưngsetenv()vàputenv()biết xử lý trường hợp này — chúng tự tạo một mảng môi trường mới khi thấyenvironrỗng.1Afterclearenv(),environis NULL; butsetenv()andputenv()handle this — they create a fresh environment array when they findenvironempty.1
Cụm khái niệm 04 Concept cluster 04
Nhảy phi cục bộ: setjmp()/longjmp() và những ràng buộc của nó Nhảy vượt qua nhiều hàm với setjmp và longjmp The nonlocal goto: setjmp()/longjmp() and its constraints
Lệnh goto của C không thể nhảy ra khỏi hàm hiện tại. Cặp hàm thư viện setjmp() và longjmp() cho phép một nhảy phi cục bộ (nonlocal goto) — nhảy từ một hàm này về một điểm trong một hàm khác đang bao nó trên ngăn xếp.110 setjmp(env) thiết lập đích cho lần nhảy sau; longjmp(env, val) nhảy về đúng điểm đó. Lần gọi setjmp() đầu tiên trả về 0; "lần trở về giả" do longjmp() gây ra trả về giá trị val — nhưng nếu val là 0 thì longjmp() thay bằng 1.1
Bình thường chương trình chạy xong hàm nào thì "quay lui" từng bước. Đôi khi, từ sâu bên trong nhiều hàm lồng nhau, ta muốn vọt thẳng một phát về một điểm đã đánh dấu từ trước, bỏ qua mọi tầng ở giữa. Cặp setjmp/longjmp làm được điều đó — như một cầu tuột thoát hiểm. Nhớ nhé: setjmp đặt điểm hẹn, longjmp vọt thẳng về đó.
C's goto cannot jump out of the current function. The library pair setjmp() and longjmp() enables a nonlocal goto — a jump from one function back to a point in a different function enclosing it on the stack.110 setjmp(env) establishes the target for a later jump; longjmp(env, val) jumps back to it. The initial setjmp() returns 0; the "faked return" caused by longjmp() supplies val — but if val is 0, longjmp() substitutes 1.1
Định nghĩa. setjmp() lưu thông tin về môi trường thực thi hiện tại vào env (kiểu jmp_buf) — trong đó có bản sao của con trỏ lệnh (program counter) và con trỏ ngăn xếp (stack pointer). Khi longjmp() được gọi, nó khôi phục các thanh ghi này, nên luồng điều khiển quay về đúng điểm setjmp(), như thể setjmp() vừa trở về lần thứ hai.110
Nói đơn giản. setjmp chụp lại "máy đang ở đâu" rồi cất vào một cái hộp. longjmp mở hộp ra và đưa máy quay lại đúng chỗ đó. Nhớ nhé: setjmp chụp ảnh vị trí, longjmp dán máy về đúng ảnh ấy.
Definition. setjmp() saves information about the current execution environment into env (type jmp_buf) — including a copy of the program counter and the stack pointer. When longjmp() is called, it restores these registers, so control returns to the setjmp() point as though setjmp() had returned a second time.110
#include <setjmp.h>
jmp_buf env;
if (setjmp(env) == 0) {
/* lần đầu: đi tiếp... */
} else {
/* vừa bị longjmp nhảy về */
}
... longjmp(env, 1);
if (setjmp(env) == 0) {
/* lần đầu */
} else {
/* bị nhảy về */
}
longjmp(env, 1);
#include <setjmp.h>
jmp_buf env;
if (setjmp(env) == 0) {
/* first time: proceed... */
} else {
/* arrived here via longjmp */
}
... longjmp(env, 1);
-
two returns
Một setjmp(), hai lần "trở về" Một lần gọi setjmp nhưng trả về hai lần One setjmp(), two "returns"
Điểm lạ lùng nhất: một lời gọi
setjmp()dường như trả về hai lần. Lần đầu (lúc thiết lập đích) trả về 0; lần sau (khi cólongjmp()) trả vềval. Bằng cách dùng các giá trịvalkhác nhau, ta phân biệt được "đến từ đâu" và xử lý tương ứng.1Gọi
setjmpmột lần, nhưng nó như trả lời hai lần: lần đầu trả 0 (đang đặt điểm hẹn), lần sau trả về một số khác khi có ai đó nhảy về. Nhờ con số đó mà bạn biết mình vừa quay về từ đâu. Nhớ nhé: thấy setjmp trả 0 là lần đầu, khác 0 là vừa bị nhảy về.The strangest point: a single
setjmp()call appears to return twice. The first time (establishing the target) it returns 0; later (on alongjmp()) it returnsval. By using differentvalvalues, you can tell "where you came from" and act accordingly.1Bạn gặp nó ở đâuWhere you meet it Khi cần thoát khỏi một chuỗi hàm lồng sâu để xử lý một lỗi, thay vì trả mã lỗi ngược từng tầng một.1 Khi cần thoát nhanh khỏi nhiều hàm lồng nhau để báo một lỗi, không phải trả ngược từng tầng. When you need to escape a deeply nested chain of functions to handle an error, instead of returning an error code up one level at a time.1 -
volatile
Bẫy tối ưu hóa: biến trong thanh ghi có thể sai giá trị Trình tối ưu có thể làm biến cục bộ sai sau khi nhảy về The optimizer trap: register variables may be wrong
Đây là ràng buộc quan trọng nhất của
longjmp(). Trình biên dịch tối ưu có thể giữ một số biến cục bộ trong thanh ghi CPU thay vì RAM. Vìlongjmp()khôi phục các thanh ghi về giá trị đã lưu lúcsetjmp(), những biến "được tối ưu" này có thể mang giá trị sai sau khi nhảy về. Biến khai báovolatilethì được miễn — trình tối ưu không đụng vào, nên giữ đúng giá trị.110Để chạy nhanh, trình biên dịch đôi khi giữ biến trong thanh ghi (chỗ cực nhanh của CPU). Nhưng khi
longjmpkhôi phục thanh ghi về lúc trước, những biến ấy có thể bị trả về giá trị cũ, sai. Cách tránh: ghi chữvolatiletrước biến để trình biên dịch "để yên" nó. Nhớ nhé: biến muốn giữ đúng qua longjmp thì khai volatile.This is
longjmp()'s most important constraint. An optimizing compiler may keep some local variables in CPU registers rather than RAM. Becauselongjmp()restores the registers to their values saved atsetjmp(), such "optimized" variables may hold the wrong value after the jump. A variable declaredvolatileis exempt — the optimizer leaves it alone, so it keeps the correct value.110Bạn gặp nó ở đâuWhere you meet it Khi cùng một chương trình cho kết quả khác nhau lúc biên dịch có-Ovà không có — rất thường vì thiếuvolatiletrên một biến cục bộ bịlongjmp()ảnh hưởng.1 Khi bật tối ưu (-O) thì chương trình chạy sai, tắt thì đúng — hãy nghĩ tới chữ volatile còn thiếu. When the same program gives different results with and without-O— very often because a local affected bylongjmp()lacksvolatile.1Lời khuyên khả chuyểnA portability tip
Vì mỗi trình biên dịch tối ưu theo cách khác nhau, chương trình khả chuyển nên dùng
volatilecho mọi biến cục bộ mà giá trị của nó cần được bảo toàn qua mộtlongjmp().110Because different compilers optimize differently, portable programs should usevolatilefor all local variables whose values must survive alongjmp().110Nơi setjmp() được phép xuất hiệnWhere setjmp() may appear
SUSv3 và C99 giới hạn
setjmp()chỉ được dùng ở một số ngữ cảnh nhất định (ví dụ làm toàn bộ biểu thức điều kiện của mộtif/while, hoặc một câu lệnh độc lập) — dùng nó trong biểu thức phức tạp hơn là hành vi không xác định.110SUSv3 and C99 restrictsetjmp()to certain contexts only (for example, as the entire controlling expression of anif/while, or a standalone statement) — using it in a more complex expression is undefined behavior.110 -
abusing longjmp
Dùng cẩn thận — và không nhảy vào hàm đã kết thúc Đừng nhảy về một hàm đã quay lui xong Use with care — never jump into a finished function
Tác giả cảnh báo rằng lạm dụng
longjmp()làm chương trình khó đọc và khó bảo trì; nên dùng dè sẻn. Một ràng buộc nghiêm ngặt: không đượclongjmp()về mộtsetjmp()nằm trong hàm đã trở về rồi — khung ngăn xếp của nó không còn, và hành vi là không xác định.1Công cụ này mạnh nhưng dễ làm mã rối; dùng ít thôi. Quan trọng: đừng nhảy về một điểm hẹn nằm trong hàm mà đã chạy xong và quay lui — điểm hẹn đó không còn tồn tại nữa. Nhớ nhé: chỉ nhảy về nơi vẫn còn "sống" trên ngăn xếp.
The author warns that abusing
longjmp()makes programs hard to read and maintain; use it sparingly. One strict rule: neverlongjmp()back to asetjmp()in a function that has already returned — its stack frame is gone, and the behavior is undefined.1Bạn gặp nó ở đâuWhere you meet it Trong trình xử lý tín hiệu: gọilongjmp()từ một handler lồng nhau là hành vi không xác định — đó là lý do có cặpsigsetjmp()/siglongjmp()(Chương 21).110 Khi dùng trong bộ xử lý tín hiệu — hãy dùng cặp an toàn hơn là sigsetjmp/siglongjmp. In signal handlers: callinglongjmp()from a nested handler is undefined — which is why thesigsetjmp()/siglongjmp()pair exists (Chapter 21).110
Ba ngộ nhận thường gặp Three common misconceptions
“Chương trình và tiến trình là một” “A program and a process are the same thing”
Ngộ nhận: tệp thực thi chính là tiến trình.
Thực tế: chương trình là một tệp mô tả cách dựng tiến trình; tiến trình là phiên bản đang chạy. Một chương trình có thể sinh ra nhiều tiến trình cùng lúc.1
Myth: the executable file is the process.
Reality: a program is a file describing how to build a process; the process is the running instance. One program can spawn many processes at once.1
“Biến toàn cục nào cũng nằm chung một chỗ” “All global variables live together”
Ngộ nhận: mọi biến toàn cục đều chiếm chỗ như nhau trong tệp và trong bộ nhớ.
Thực tế: biến có khởi tạo nằm ở segment data (và lưu trong tệp); biến không khởi tạo nằm ở BSS và không chiếm chỗ trên đĩa — chỉ ghi vị trí và kích thước.1
Myth: every global takes equal space in the file and in memory.
Reality: initialized globals live in the data segment (and are stored in the file); uninitialized ones live in BSS and take no disk space — only their location and size are recorded.1
“longjmp() khôi phục đúng mọi biến cục bộ” “longjmp() restores all local variables correctly”
Ngộ nhận: sau longjmp(), mọi biến cục bộ giữ nguyên giá trị như trước khi nhảy.
Thực tế: biến được tối ưu vào thanh ghi có thể mang giá trị sai; muốn chắc đúng phải khai volatile. Khác biệt chỉ lộ ra khi bật -O.110
Myth: after longjmp(), every local keeps the value it had before the jump.
Reality: variables optimized into registers may hold the wrong value; to be sure, declare them volatile. The difference only shows up with -O.110
Tự kiểm Self-check
Năm câu hỏi để biết bạn đã nắm giải phẫu của một tiến trình Năm câu hỏi kiểm tra nhanh Five questions to check you grasped a process's anatomy
Trả lời được những câu này nghĩa là bạn đã nắm cấu trúc, danh tính và bộ nhớ của một tiến trình — nền cho các chương về tạo và kết thúc tiến trình.
Nếu trả lời được, nghĩa là bạn đã hiểu chương này.
Answering these means you grasp a process's structure, identity, and memory — the groundwork for the chapters on process creation and termination.
-
Phân biệt chương trình và tiến trình. Vì sao một chương trình có thể sinh ra nhiều tiến trình?
Gợi ý sâu hơn: từ góc nhìn nhân, một tiến trình gồm những thành phần nào ngoài bộ nhớ không gian người dùng?
-
Kể năm segment chính trong bộ nhớ một tiến trình và vai trò của từng cái. Vì sao text để chỉ-đọc và dùng chung?
Gợi ý sâu hơn: vì sao biến chưa khởi tạo (BSS) lại không cần chiếm chỗ trên đĩa?
-
Bộ nhớ ảo hoạt động ra sao? Giải thích các khái niệm trang, resident set, và page fault.
Gợi ý sâu hơn: điều gì cho phép nhiều tiến trình chạy cùng một chương trình chỉ cần một bản mã trong RAM?
-
argv[0]chứa gì, và vì saoargv[argc]luôn là NULL lại hữu ích? Môi trường của một tiến trình đến từ đâu?Gợi ý sâu hơn: khác biệt giữa
setenv()vàputenv()về việc sao chép đối số là gì? -
Một lời gọi
setjmp()trả về giá trị gì ở lần đầu và sau mộtlongjmp()? Vì sao đôi khi phải khai biếnvolatile?Gợi ý sâu hơn: vì sao không được
longjmp()về một hàm đã trở về?
Chương trình và tiến trình khác nhau chỗ nào?
Bộ nhớ của một tiến trình chia thành những ngăn nào?
Vì sao máy không cần giữ cả chương trình trong RAM cùng lúc?
Khi gõ lệnh kèm vài chữ phía sau, chương trình nhận chúng qua đâu?
Cặp setjmp/longjmp dùng để làm gì?
Distinguish a program from a process. Why can one program spawn many processes?
Name the five main memory segments of a process and each one's role. Why is text read-only and sharable?
How does virtual memory work? Explain pages, the resident set, and page faults.
What does
argv[0]hold, and why is it useful thatargv[argc]is always NULL? Where does a process's environment come from?What does a
setjmp()call return on its first call and after alongjmp()? Why must some variables be declaredvolatile?
Khép lại chương Closing the chapter
Một tiến trình là chương trình lúc đang sống: có danh tính (PID/PPID), có một bản đồ bộ nhớ gồm các segment trên nền bộ nhớ ảo, nhận đối số và môi trường từ bên ngoài, và đôi khi điều khiển luồng bằng những lối nhảy đặc biệt như
longjmp(). Nắm được giải phẫu này, bạn đã sẵn sàng cho câu hỏi lớn kế tiếp — làm sao một tiến trình được sinh ra và kết thúc.1Tiến trình là chương trình lúc đang chạy: có tên riêng, có các ngăn nhớ gọn gàng, nhận đầu vào từ ngoài. Hiểu bộ khung này là đủ để đi tiếp sang chuyện tiến trình ra đời và kết thúc. Nhớ nhé: hiểu một tiến trình là hiểu một chương trình lúc nó đang thở.
A process is a program brought to life: it has an identity (PID/PPID), a memory map of segments over virtual memory, inputs from the outside, and occasionally special control transfers like
longjmp(). Grasp this anatomy and you are ready for the next big question — how a process is born and dies.1