# linux_go **Repository Path**: fyang0906/linux_go ## Basic Information - **Project Name**: linux_go - **Description**: Linux 系统编程学习笔记 - **Primary Language**: C/C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-07-02 - **Last Updated**: 2025-03-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Linux 系统编程 ## 0-1. Linux 系统概要 **计算机系统由 “身躯” 和 “灵魂” 两部分组成** - “躯体”:构成计算机系统的电子设备(硬件) - “灵魂”:指挥 “躯体” 完成 “动作” 的指令序列(软件) “躯体” 核心:中央处理器(CPU) “灵魂” 核心:操作系统(OS) **再论计算机系统** > 计算机系统的诞生是为了执行数据处理(计算任务)。那么,如何获取计算任务,并反馈任务执行结果? 数据输入与输出是计算机系统不可或缺的部分 ![1-linux_os_intro_1.png](image/1-linux_os_intro_1.png) **计算任务执行流程** - 通过交互设备或网络向计算机系统发起请求 - 根据请求将任务指令加载进入内存 - CPU从内存中取指令,并逐条执行 - 计算任务的最终结果暂存入内存 - 内存数据通过交互设备或网络进行反馈(也可以写入外存) ![1-linux_os_intro_2.png](image/1-linux_os_intro_2.png) **操作系统与进程概要** 什么是程序? - 程序的本质是指令和数据的集合 - 指令:指示CPU执行动作的命令 - 数据:CPU执行动作的操作目标 程序的分类: - 应用程序:用户可以直接使用,为用户提供直接帮助的程序 - 程序中间件:多数应用程序的通用功能,用于辅助应用程序的运行 - 操作系统:直接操作硬件设备,并为应用程序与程序中间件提供运行环境 **当代计算系统架构** ![1-linux_os_intro_3.png](image/1-linux_os_intro_3.png) **进程-程序的执行** - 通常情况下,程序在操作系统上以进程为单位运行 - 每个程序运行后由一个或多个进行构成 - 进程是操作系统任务的基本单元,也是系统资源的基本分配单元 - 程序是 “死” 的,进程是 “活” 的 - 程序的本质是二进制数据,不加载执行就没有任何价值 - 进程是计算机系统对程序的一次加载执行,即:执行计算任务的过程 **程序 VS 进程** ![1-linux_os_intro_4.png](image/1-linux_os_intro_4.png) **应用程序与外部设备** - 多数情况下,应用程序需要协助外部设备才能完成计算任务 - 外部设备:除CPU与内存之外的其它计算机硬件(如:硬盘,网卡,显卡) ![1-linux_os_intro_5.png](image/1-linux_os_intro_5.png) **应用程序设备访问** - 直接访问 - 开发成本高:应用开发者必须熟悉各类外设的硬件特性 - 开发周期长:业务逻辑 + 设备逻辑 - 应用场景难:其它应用程序可能同时访问外设 - 间接访问 - 应用程序通过某软件层(驱动程序)接口以统一方式访问外设 **设备驱动程序** - 设备驱动程序是外设访问接口,对应用程序提供统一的外设访问方式 ![1-linux_os_intro_6.png](image/1-linux_os_intro_6.png) **Linux 设备驱动模型** - 抽象各种外设的共性,简化设备驱动开发方式 - 设备类型:字符设备,块设备,网络设备等 - 对于同一类型的设备,可以通过统一接口进行访问 ![1-linux_os_intro_7.png](image/1-linux_os_intro_7.png) 驱动程序: - 对上:`write()` `read()` - 对下:为操作系统设备准备的函数 **存在的问题** - 设备驱动程序并非唯一访问外设的方式 > 如何限制进程必须按照规则通过驱动程序访问外部设备? **Linux系统的工作模式** ![1-linux_os_intro_8.png](image/1-linux_os_intro_8.png) - 用户模式(User Mode) - 执行应用程序私有代码,受限制的访问内存,无法直接访问外部设备 - 内核模式(Kernel Mode) - 执行内核代码,可以访问所有硬件资源,可立即暂停进程的执行 - 绝大多数设备驱动程序执行于内核模式 > **内核职责:** 以统一的方式有序的分配硬件资源,保证用户任务按照期望的方式执行 **Linux系统的工作模式:系统调用(System Call)** - 应用程序与操作系统内核直接的接口(表现形式为函数) - 系统调用决定了应用程序如何与内核打交道 - 为什么需要系统调用? - 系统资源有限,需要统一有序的调配 - 多个进程可能同时访问同一资源,进而产生冲突 - 一些特定的功能必须由操作系统内核完成(如:精确延时) - ... - 进程系统调用后,由用户模式切换到内核模式(执行内核代码) - 工作模式的转变通常由中断触发(不同于普通函数调用) - 用户进程通过系统调用请求内核完成资源分配,硬件访问等操作 - 所有进程请求集中到内核,内核可统一调度处理,协调进程的执行 ![1-linux_os_intro_9.png](image/1-linux_os_intro_9.png) ![1-linux_os_intro_10.png](image/1-linux_os_intro_10.png) ## 0-2. 深入理解系统调用 **揭示系统API的奥密** Linux系统架构 ![2-api_1.png](image/2-api_1.png) **模式切换的本质(系统调用的本质)** - 系统模式切换依赖于CPU提供的工作方式 - 一般来说,大部分CPU至少具有两种工作方式 - 高特权级(Ring 0)内核模式:可以访问任意的数据,包括内存与外设 - 低特权级(Ring 3)用户模式:只能受限访问内存,并且不允许访问外围设备,可被打断 - 系统模式切换通过执行特殊的CPU指令发起(`int 0x80`) - 应用程序(进程)无法直接切换CPU的工作方式 - 系统调用是应用程序(进程)请求模式切换的唯一方式 ![2-api_2.png](image/2-api_2.png) **系统调用的真面目** ```C char* s = "Hello, World!"; int l = 13; asm volatile ( "movl $4, %%eax\n" //指定编号为 4 的系统调用 (sys_write) "movl $1, %%ebx\n" //指定 sys_write 的输出目标 1 为标准输出(stdout) "movl %0, %%ecx\n" //指定输出字符串地址 "movl %1, %%edx\n" //指定输出字符串长度 "int $0x80 \n" //执行系统调用 : //忽略输出参数 : "r"(s), "r"(l) : "eax", "ebx", "ecx", "edx"); //保留寄存器,不用于关联变量 ``` **系统API的真面目** ```C void print(const char *s, int l) { asm volatile( "movl $4, %%eax\n" // sys_write "movl $1, %%ebx\n" "movl %0, %%ecx\n" "movl %1, %%edx\n" "int $0x80 \n" // 80H Service : : "r"(s), "r"(l) // parameter : "eax", "ebx", "ecx", "edx"); } ``` **系统调用和系统API实现示例** ```C #define SysCall(type, cmd, param1, param2) asm volatile( \ "movl $4, %%eax\n" \ "movl $1, %%ebx\n" \ "movl %0, %%ecx\n" \ "movl %1, %%edx\n" \ "int $0x80 \n" \ : \ : "r"(param1), "r"(param2) \ : "eax", "ebx", "ecx", "edx") ``` ```C void RegApp(const char *name, void (*tmain)(), byte pri) { if (name && tmain) { AppInfo info = {0}; info.name = name; info.pri = pri; info.tmain = tmain; SysCall(0, 2, &info, 0); } } ``` - ***直接使用系统调用*** > *[0-linux_basics/2-api/program.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/program.c)* ```C void print(const char *s, int l); void exit(int code); void program() { print("Hello world!\n", 13); exit(0); } void print(const char *s, int l) { asm volatile( "movl $4, %%eax\n" // sys_write "movl $1, %%ebx\n" "movl %0, %%ecx\n" "movl %1, %%edx\n" "int $0x80 \n" // 80H Service : : "r"(s), "r"(l) // parameter : "eax", "ebx", "ecx", "edx"); } void exit(int code) { asm volatile( "movl $1, %%eax\n" // sys_exit "movl $0, %%ebx\n" "int $0x80 \n" // 80H Service : : "r"(code) : "eax", "ebx"); } ``` - 编译:`gcc program.c -o program.out` ```bash ☁ 2-api [master] ⚡ gcc program.c -o program.out program.c: In function ‘exit’: program.c:32:1: warning: ‘noreturn’ function does return 32 | } | ^ program.c: Assembler messages: program.c:14: Error: unsupported instruction `mov' ``` > **错误原因:** 当前操作系统为64位,而上述代码中嵌入的汇编代码是32位的,因此使用32位的编译方式 `gcc -m32 program.c -o program.out` > 编译:`gcc -m32 program.c -o program.out` ```bash ☁ 2-api [master] ⚡ gcc -m32 program.c -o program.out program.c: In function ‘exit’: program.c:32:1: warning: ‘noreturn’ function does return 32 | } | ^ /usr/bin/ld: cannot find Scrt1.o: No such file or directory /usr/bin/ld: cannot find crti.o: No such file or directory /usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/9/libgcc.a when searching for -lgcc /usr/bin/ld: cannot find -lgcc /usr/bin/ld: skipping incompatible /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 when searching for libgcc_s.so.1 /usr/bin/ld: cannot find libgcc_s.so.1 /usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/9/libgcc.a when searching for -lgcc /usr/bin/ld: cannot find -lgcc collect2: error: ld returned 1 exit status ``` > **Tips:** 从上述报错信息看出没有安装32位编译器相关的库文件,执行以下命令安装: > > - `sudo apt-get install build-essential module-assistant` > - `sudo apt-get install gcc-multilib g++-multilib` - 编译:`gcc -m32 program.c -o program.out` ```bash ☁ 2-api [master] ⚡ gcc -m32 program.c -o program.out program.c: In function ‘exit’: program.c:32:1: warning: ‘noreturn’ function does return 32 | } | ^ /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib32/Scrt1.o: in function `_start': (.text+0x2c): undefined reference to `main' collect2: error: ld returned 1 exit status ``` > 如上述报错找不到 `main` 函数,使用 `-e proram` 指定 `program` 作为入口函数 - 编译:`gcc -m32 -e program program.c -o program.out` ```bash ☁ 2-api [master] ⚡ gcc -m32 -e program program.c -o program.out program.c: In function ‘exit’: program.c:32:1: warning: ‘noreturn’ function does return 32 | } | ^ /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib32/Scrt1.o: in function `_start': (.text+0x2c): undefined reference to `main' collect2: error: ld returned 1 exit status ``` > 指定不需要 `_start` 函数 `-fno-builtin -nostartfiles` - 编译:`gcc -m32 -e program -fno-builtin -nostartfiles program.c -o program.out` - 运行:`./program.out` ```bash ☁ 2-api [master] ⚡ ./program.out Hello world! ``` - **使用第三方库** > *[0-linux_basics/2-api/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/main.c)* ```C #include int main(void) { printf("Hello, World!\n"); return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out` ```bash ☁ 2-api [master] ⚡ ./main.out Hello, World! ``` ![2-api_3.png](image/2-api_3.png) > **值得思考的问题:** 如何判断一个应用程序(进程)触发了系统调用? **strace - 系统调用探测器** - strace 用于监控进程与内核的交互(监控系统调用) - strace 用于追踪进程内部状态(定位运行时问题) - strace 按序输出进程运行时系统调用名称,参数和返回值 ``` **** Let's learn how to interpret strace output **** 11999 execve("/bin/ls", ["ls","home/bork/blah"]) = 0 ----- ----- ---------------------------------- ---- 1 2 3 4 1. The process ID (include when you run strace -f) 2. The name of the system call (execve starts programs) 3. The system call's arguments, in this case a program to start and the arguments to start it with 4. The return value ``` - 使用 strace ```bash strace ./program.out ``` ```bash ☁ 2-api [master] ⚡ strace ./program.out execve("./program.out", ["./program.out"], 0x7fff88b09dd0 /* 41 vars */) = 0 strace: [ Process PID=13808 runs in 32 bit mode. ] brk(NULL) = 0x58335000 arch_prctl(0x3001 /* ARCH_??? */, 0xffc36018) = -1 EINVAL (Invalid argument) access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7f0e000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) set_thread_area({entry_number=-1, base_addr=0xf7f0e9c0, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12) mprotect(0x56582000, 4096, PROT_READ) = 0 write(1, "Hello world!\n", 13Hello world! ) = 13 exit(0) = ? +++ exited with 0 +++ ``` - **execve** - execute program (加载执行应用程序) - **brk, sbrk** - change data segment size (创建数据段) **进阶论证系统调用** - 将 strace 的输出存放在 program.log 文件中:`strace -o ./program.log ./program.out` - 将 strace 的输出存放在 main.log 文件中:`strace -o main.log ./main.out` > 使用不同编程语言编写 `Hello World` 程序并用 strace 进行分析 - 使用C++编写 `Hello World` 程序 > *[0-linux_basics/2-api/main.cpp](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/main.cpp)* ```C++ #include using namespace std; int main() { cout << "Hello, World!" << endl; return 0; } ``` - 编译:`g++ main.cpp -o cpp.out` - 运行:`./cpp.out` ```bash ☁ 2-api [master] ⚡ ./cpp.out Hello, World! ``` - 使用 strace 查看 cpp.out 使用的系统调用,并生成 cpp.log 文件 ```bash strace -o cpp.log ./cpp.out ``` > 从 strace 的输出信息可以看到,直接使用系统调用,使用C语言和C++语言编写的 Hello World 程序都使用了 `write` 系统调用 - 使用 `ldd` 命令分别查看 `main.out`,`cpp.out`, `program.out` 的依赖库 ```bash ☁ 2-api [master] ⚡ ldd main.out linux-vdso.so.1 (0x00007ffca39b6000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0729f15000) /lib64/ld-linux-x86-64.so.2 (0x00007f072a118000) ``` ```bash ☁ 2-api [master] ⚡ ldd cpp.out linux-vdso.so.1 (0x00007ffe8445a000) libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f6b19078000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6b18e86000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6b18d37000) /lib64/ld-linux-x86-64.so.2 (0x00007f6b1926b000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f6b18d1c000) ``` ```bash ☁ 2-api [master] ⚡ ldd program.out statically linked ``` > 从上述输出结果可以初步得到结论,依赖的类库(软件中间件)越多,strace 的输出信息就越复杂 - 分别使用 Python 和 Perl 编写 Hello World 程序 > *[0-linux_basics/2-api/main.py](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/main.py)* ```Python print("Hello World!") ``` - 编译运行:`python3 main.py` ```bash ☁ 2-api [master] ⚡ python3 main.py Hello World! ``` > *[0-linux_basics/2-api/main.pl](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/main.pl)* ```perl print "Hello, World!\n"; ``` - 编译运行:`perl main.pl` ```bash ☁ 2-api [master] ⚡ perl main.pl Hello, World! ``` - 使用 strace 查看分析 Python 程序和 Perl 程序 > - `strace -o py.log python3 main.py` > - `strace -o pl.log perl main.pl` > **Tips:** > > - 从输出结果可以看到 Python 程序和 Perl 程序也使用了系统调用 `write` > - 编译型语言比解释型语言执行速度快 **strace程序性能分析** - 使用 strace 打印程序系统调用的时间:`strace -T -tt ./main.out` > - `-t` 打印进程内系统调用时间戳 > - `-tt` 打印进程内系统调用时间戳(毫秒) > - `-T` 打印系统调用时间 ```bash ☁ 2-api [master] ⚡ strace -T -tt ./main.out 21:52:51.729389 execve("./main.out", ["./main.out"], 0x7ffd33a4b720 /* 41 vars */) = 0 <0.000541> 21:52:51.730341 brk(NULL) = 0x5596773f6000 <0.000200> 21:52:51.730862 arch_prctl(0x3001 /* ARCH_??? */, 0x7fff96e0ecf0) = -1 EINVAL (Invalid argument) <0.000155> 21:52:51.731368 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) <0.000183> 21:52:51.731809 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 <0.000156> 21:52:51.732242 fstat(3, {st_mode=S_IFREG|0644, st_size=39878, ...}) = 0 <0.000091> 21:52:51.732637 mmap(NULL, 39878, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8c7c444000 <0.000136> 21:52:51.733020 close(3) = 0 <0.000131> 21:52:51.733399 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 <0.000120> 21:52:51.733728 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300A\2\0\0\0\0\0"..., 832) = 832 <0.000077> 21:52:51.733985 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 <0.000064> 21:52:51.734204 pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32 <0.000072> 21:52:51.734427 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\7\2C\n\357_\243\335\2449\206V>\237\374\304"..., 68, 880) = 68 <0.000093> 21:52:51.734739 fstat(3, {st_mode=S_IFREG|0755, st_size=2029592, ...}) = 0 <0.000064> 21:52:51.734928 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8c7c442000 <0.000168> 21:52:51.735424 pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784 <0.000087> 21:52:51.735787 pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32 <0.000075> 21:52:51.736030 pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\7\2C\n\357_\243\335\2449\206V>\237\374\304"..., 68, 880) = 68 <0.000095> 21:52:51.736473 mmap(NULL, 2037344, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8c7c250000 <0.000104> 21:52:51.736758 mmap(0x7f8c7c272000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f8c7c272000 <0.000143> 21:52:51.737066 mmap(0x7f8c7c3ea000, 319488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19a000) = 0x7f8c7c3ea000 <0.000086> 21:52:51.737301 mmap(0x7f8c7c438000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f8c7c438000 <0.000087> 21:52:51.737549 mmap(0x7f8c7c43e000, 13920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8c7c43e000 <0.000111> 21:52:51.737833 close(3) = 0 <0.000076> 21:52:51.738098 arch_prctl(ARCH_SET_FS, 0x7f8c7c443540) = 0 <0.000086> 21:52:51.738499 mprotect(0x7f8c7c438000, 16384, PROT_READ) = 0 <0.000175> 21:52:51.738879 mprotect(0x55967630b000, 4096, PROT_READ) = 0 <0.000081> 21:52:51.739123 mprotect(0x7f8c7c47b000, 4096, PROT_READ) = 0 <0.000079> 21:52:51.739351 munmap(0x7f8c7c444000, 39878) = 0 <0.000095> 21:52:51.739674 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x5), ...}) = 0 <0.000078> 21:52:51.739973 brk(NULL) = 0x5596773f6000 <0.000068> 21:52:51.740195 brk(0x559677417000) = 0x559677417000 <0.000077> 21:52:51.740442 write(1, "Hello, World!\n", 14Hello, World! ) = 14 <0.000095> 21:52:51.740716 exit_group(0) = ? 21:52:51.741043 +++ exited with 0 +++ ``` - 使用 strace 统计程序系统调用的次数和总时间:`strace -c python3 main.py` > - `-c` 统计程序系统调用的次数和总时间 ```bash ☁ 2-api [master] ⚡ strace -c python3 main.py Hello World! % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 27.48 0.004509 30 146 27 stat 13.68 0.002244 28 79 read 9.46 0.001552 17 91 fstat 8.45 0.001386 27 50 mmap 8.21 0.001347 24 56 2 openat 8.19 0.001344 23 57 close 6.45 0.001058 15 68 rt_sigaction 5.17 0.000849 11 74 3 lseek 3.08 0.000506 12 40 34 ioctl 2.72 0.000447 27 16 getdents64 2.29 0.000376 34 11 mprotect 1.30 0.000214 13 16 brk 1.01 0.000166 33 5 munmap 0.43 0.000070 23 3 2 readlink 0.29 0.000048 48 1 prlimit64 0.28 0.000046 46 1 set_robust_list 0.21 0.000035 11 3 dup 0.19 0.000031 31 1 rt_sigprocmask 0.17 0.000028 28 1 write 0.17 0.000028 9 3 fcntl 0.16 0.000027 13 2 1 arch_prctl 0.16 0.000026 26 1 set_tid_address 0.16 0.000026 26 1 getrandom 0.15 0.000025 25 1 futex 0.09 0.000015 15 1 lstat 0.02 0.000003 3 1 getcwd 0.00 0.000000 0 8 pread64 0.00 0.000000 0 1 1 access 0.00 0.000000 0 1 execve 0.00 0.000000 0 1 sysinfo 0.00 0.000000 0 1 getuid 0.00 0.000000 0 1 getgid 0.00 0.000000 0 1 geteuid 0.00 0.000000 0 1 getegid 0.00 0.000000 0 3 sigaltstack ------ ----------- ----------- --------- --------- ---------------- 100.00 0.016406 747 70 total ``` **strace程序逆向分析** > *[0-linux_basics/2-api/fcopy.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/2-api/fcopy.c)* ```C++ #include #include #include #include #include #include #include int file_copy(const char *dst, const char *src) { int dfd = open(dst, O_WRONLY | O_CREAT, 0600); int sfd = open(src, O_RDONLY); int ret = 0; if (dfd == -1) { ret = -1; } else if (sfd == -1) { ret = -2; } else if ((dfd != -1) && (sfd != -1)) { char buf[512] = {0}; int len = 0; while ((len = read(sfd, buf, sizeof(buf))) > 0) { write(dfd, buf, len); } close(sfd); close(dfd); } return ret; } int main(int argc, char **argv) { int err = 0; if (argc != 3) { printf("Usage: %s \n", argv[0]); err = 1; } else { err = file_copy(argv[1], argv[2]); if (err == 0) { printf("File copied successfully\n"); } else { printf("Error copying file: %d\n", err); } } return 0; } ``` - 编译:`gcc fcopy.c -o fcopy.out` - 运行 fcopy.out 拷贝 cpp.out 生成 new_cpp.out :`./fcopy.out new_cpp.out cpp.out` ```bash ☁ 2-api [master] ⚡ ./fcopy.out new_cpp.out cpp.out File copied successfully ``` - 运行 new_cpp.out :`./new_cpp.out` ```shell ☁ 2-api [master] ⚡ ./new_cpp.out zsh: permission denied: ./new_cpp.out ``` - 为 new_cpp.out 添加执行权限:`sudo chmod +x new_cpp.out`;并运行 new_cpp.out ```shell ☁ 2-api [master] ⚡ ./new_cpp.out Hello, World! ``` - 使用 strace 逆向分析 `fcopy.out` 1. 运行命令:`strace -o fcopy.log ./fcopy.out new_cpp.out cpp.out` 2. 打开 `fcopy.log` 进行分析 ```bash #... openat(AT_FDCWD, "new_cpp.out", O_WRONLY|O_CREAT, 0600) = 3 openat(AT_FDCWD, "cpp.out", O_RDONLY) = 4 #... ``` ```bash #... read(4, "\10\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\345\0\0\0\16\0\0\0\3\0\0\0\0\0\0\0"..., 512) = 512 write(3, "\10\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\345\0\0\0\16\0\0\0\3\0\0\0\0\0\0\0"..., 512) = 512 read(4, "\10\0\0\0\0\0\0\0\30\0\0\0\0\0\0\0\t\0\0\0\3\0\0\0\0\0\0\0\0\0\0\0"..., 512) = 144 write(3, "\10\0\0\0\0\0\0\0\30\0\0\0\0\0\0\0\t\0\0\0\3\0\0\0\0\0\0\0\0\0\0\0"..., 144) = 144 #... ``` ```bash write(1, "File copied successfully\n", 25) = 25 ``` > 根据上述 strace 的输出信息可以大致分析出程序 `fcopy.out` 是如何实现的 - 使用 strace 分析 `fcopy.out` 异常处理的情况 1. 运行命令 `./fcopy.out /new-file/new_cpp.out cpp.out` ```bash Error copying file: -1 ``` 2. 运行命令:`strace -o fcopy_err.log ./fcopy.out /new-file/new_cpp.out cpp.out` 3. 打开 `fcopy_err.log` 进行分析 ```bash # ... openat(AT_FDCWD, "/new-file/new_cpp.out", O_WRONLY|O_CREAT, 0600) = -1 ENOENT (No such file or directory) # ... ``` ```bash # ... write(1, "Error copying file: -1\n", 25) = 25 # ... ``` > 根据上述 strace 的输出信息可以分析出程序 `fcopy.out` 是如何处理异常情况的 **strace程序数据分析** - 如何查看 `fcopy.out` 程序在执行过程中所涉及的具体数据是什么? 1. 执行命令:`strace -o fcopy_data_log ./fcopy.out new_program.out program.out` 2. 打开 `fcopy_data_log` 进行分析 > - `-s 1024` -- 定义跟踪数据的字节数 > - `-xx` -- 跟踪数据以十六进制显示 > - `-e read=4` -- 只关注文件描述符为4,系统调用read的数据 > - `-e trace=file,read,close` -- 只关注与文件相关的系统调用 - 执行命令:`strace -s 1024 -o fcopy_data_log ./fcopy.out new_program.out program.out` - 执行命令:`strace -s 1024 -x -o fcopy_data_log ./fcopy.out new_program.out program.out` - 执行命令:`strace -e read=4 -o fcopy_data_log ./fcopy.out new_program.out program.out` - 执行命令:`strace -e trace=file,read,close -o fcopy_data_log ./fcopy.out new_program.out program.out` **strace – 用法详解** - **`-c`** -- count time, calls, and errors for each syscall and report summary - **`-f`** -- follow forks - **`-ff`** -- with output into separate files - **`-F`** -- attempt to follow vforks - **`-i`** -- print instruction pointer at time of syscall - **`-q`** -- suppress messages about attaching, detaching, etc. - **`-r`** -- print relative timestamp - **`-t`** -- absolute timestamp, - **`-tt`** -- with usecs - **`-T`** -- print time spent in each syscall, - **`-v`** -- verbose mode: print unabbreviated argv, stat, termio[s], etc. args - **`-x`** -- print non-ascii strings in hex, - **`-xx`** -- print all strings in hex - **`-o`** file -- send trace output to FILE instead of stderr - **`-O`** overhead -- set overhead for tracing syscalls to OVERHEAD usecs - **`-p pid`** -- trace process with process id PID, may be repeated - **`-D`** -- run tracer process as a detached grandchild, not as parent - **`-s strsize`** -- limit length of print strings to STRSIZE chars (default 32) - **`-S sortby`** -- sort syscall counts by: time, calls, name, nothing(default time) - **`-e expr`** : 指定一个表达式,用来控制如何跟踪 - `-e trace=set` 跟踪指定的系统调用 > 如:`-e trace=open,close,rean,write` 表示只跟踪这四个系统调用 > - `-e trace=file` 跟踪有关文件操作的系统调用 - `-e trace=process` 跟踪有关进程控制的系统调用 - `-e trace=network` 跟踪与网络有关的所有系统调用 - `-e strace=signal` 跟踪所有与系统信号有关的系统调用 - `-e trace=ipc` 跟踪所有与进程通讯有关的系统调用 - `-e raw=set` 将指定的系统调用的参数以十六进制显示 - `-e signal=set` 指定跟踪的系统信号 > 如:`signal=!SIGIO(或者signal=!IO)`,表示不跟踪 SIGIO 信号 > - `-e read=set` 输出从指定文件中读出的数据 > 如:`-e read=3,5` > - `-e write=set` 输出写入到指定文件中的数据 ## 0-3. Linux 进程 > **问题:** strace 输出中的 `execve(...)` 究竟是什么? **进程生命周期** ![3-process_1.png](image/3-process_1.png) - Linux进程基本概念 - 进程是Linux任务的执行单元,也是Linux系统资源的分配单元 - 每个Linux应用程序运行后由一个或多个进程构成 - 每个Linux进程可以执行一个或多个程序 - Linux进程由多种不同状态(即:Linux进程有不同的活法) **Linux进程状态剖析** - Linux进程生命周期 - 就绪/运行状态(R):TASK_RUNNING - 阻塞状态: - 可中断(S):TASK_INTERRUPTIBLE(进程可以接收信号) - 不可中断(D):TASK_UNINTERRUPTIBLE(进程不可接收信号) - 停止状态(T):TASK_STOPPED - 退出状态:TASK_DEAD - 僵尸(X):EXIT_ZOMBIE - 死亡(Z):EXIT_DEAD ![3-process_2.png](image/3-process_2.png) - 使用 `ps` 命令查看Linux进程 ```bash ☁ linux_go [master] ⚡ ps PID TTY TIME CMD 999 pts/6 00:00:01 zsh 5746 pts/6 00:00:00 ps ``` - 查看系统中所有的进程:`ps ax` ```bash ☁ linux_go [master] ⚡ ps ax PID TTY STAT TIME COMMAND ... ... ... 6211 pts/2 S+ 0:00 sleep 1 6213 pts/6 R+ 0:00 ps ax ``` - 打印与当前命令行相关的进程信息:`ps ax | grep pts/0` ```bash ☁ linux_go [master] ⚡ ps ax | grep pts/0 532 pts/0 Ss+ 0:01 -zsh 6710 pts/6 S+ 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox pts/0 ``` - 创建一个可中断的后台阻塞进程:`sleep 120 &` ```bash ☁ linux_go [master] ⚡ sleep 120 & [1] 7387 ``` - 查看进程状态:`ps ax | grep sleep` ```bash ☁ linux_go [master] ⚡ ps ax | grep sleep 8560 pts/6 SN 0:00 sleep 120 9926 pts/6 S+ 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox sleep ``` - 杀死该进程:`kill 8560` ```bash ☁ linux_go [master] ⚡ kill 8560 [1] + 8560 terminated sleep 120 ``` > *[0-linux_basics/3-process/loop.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/3-process/loop.c)* ```C++ int main() { while (1) { /* code */ } return 0; } ``` > 编译运行并查看程序 `loop.out` 进程状态 ```bash ☁ 3-process [master] ⚡ gcc loop.c -o loop.out ☁ 3-process [master] ⚡ ./loop.out & [1] 11954 ☁ 3-process [master] ⚡ ps ax | grep loop 11954 pts/5 RN 0:22 ./loop.out 12125 pts/5 S+ 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox loop ☁ 3-process [master] ⚡ kill 11954 [1] + 11954 terminated ./loop.out ``` **Linux进程基础知识** - 每一个进程都有一个唯一的标识符(PID) - 每个进程都是由另一个进程创建而来(即:父进程) ```C #include #include pid_t getpid(void); //获取当前进程ID pid_t getppid(void); //获取当前进程父进程ID ``` > **一些有趣的问题:** > > - 问题一:第1个进程是什么? > - 问题二:如何创建进程? **Linux进程树** - 整个Linux系统的所有进程构成一个树状结构 - 树根由内核自动创建,即:`IDLE (PID -> 0)` - 系统中的第一个进程是 `init/systemd (PID->1)` - 0 号进程创建 1 号进程,1 号进程负责完成内核部分初始化工作 - 1 号进程加载执行初始化程序,演变为用户态 1 号进程 > 打印进程树:`pstree` ```bash ☁ ~ pstree systemd─┬─ModemManager───2*[{ModemManager}] ├─NetworkManager───3*[{NetworkManager}] ├─accounts-daemon───2*[{accounts-daemon}] ├─2*[agetty] ├─atd ├─avahi-daemon───avahi-daemon ├─cron ├─dbus-daemon ├─fwupd───4*[{fwupd}] ├─inetd ├─irqbalance───{irqbalance} ├─multipathd───6*[{multipathd}] ├─networkd-dispat ├─polkitd───2*[{polkitd}] ├─rsyslogd───3*[{rsyslogd}] ├─snapd───10*[{snapd}] ├─sshd───sshd───sshd───sh─┬─code-fabdb6a30b─┬─sh───node─┬─node───11*[{node}] │ │ │ ├─node─┬─zsh───pstree │ │ │ │ └─11*[{node}] │ │ │ └─10*[{node}] │ │ └─4*[{code-fabdb6a30b}] │ └─sleep ├─systemd───(sd-pam) ├─systemd-journal ├─systemd-logind ├─systemd-network ├─systemd-resolve ├─systemd-timesyn───{systemd-timesyn} ├─systemd-udevd ├─udisksd───4*[{udisksd}] ├─unattended-upgr───{unattended-upgr} └─wpa_supplicant ``` - 打印当前进程 PID:`echo $$` ```shell ☁ ~ echo $$ 2202 ``` ```bash ☁ ~ pstree -A -p -s $$ systemd(1)---sshd(853)---sshd(1849)---sshd(1957)---sh(1958)---code-fabdb6a30b(2008)---sh(2095)---node(2099)---node(2149)---zsh(2202)---pstree(3616) ``` ```bash ☁ ~ sleep 200 & [1] 3779 ☁ ~ sleep 200 & [2] 3792 ☁ ~ sleep 200 & [3] 3802 ☁ ~ pstree -A -p -s $$ systemd(1)---sshd(853)---sshd(1849)---sshd(1957)---sh(1958)---code-fabdb6a30b(2008)---sh(2095)---node(2099)---node(2149)---zsh(2202)-+-pstree(3813) |-sleep(3779) |-sleep(3792) `-sleep(3802) ``` **Linux进程创建** - `pid_t fork(void);` - 通过当前进程创建新进程,当前进程为父进程,新进程为子进程 - `int execve(const char *pathname, char *const argv[], char *const envp[]);` - 在当前进程中执行 `pathname` 指定的程序代码 > 先创建进程,才能执行程序代码!!! - `fork()` 的工作方式 - 为子进程申请内存空间,将父进程数据完全复制到子进程空间中 - 两个进程中的程序执行位置完全一致(`fork()` 函数调用位置) - 不同之处: - 父进程:`fork()` 返回子进程PID - 子进程:`fork()` 返回0 > 通过 `fork()` 返回值判断父子进程,执行不同代码 > 下面的程序输出什么?为什么? ```C static int g_global = 0; int main() { int pid = 0; printf("Hello World!\n"); if ((pid = fork()) > 0) { g_global = 1; usleep(100); printf("g_global = %d\n", g_global); } else { g_global = 10; printf("g_global = %d\n", g_global); } return 0; } ``` > *[0-linux_basics/3-process/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/3-process/main.c)* ```C #include #include #include static int g_global = 0; int main() { int pid = 0; printf("Hello World!\n"); printf("current process id: %d\n", getpid()); if ((pid = fork()) > 0) { g_global = 1; usleep(100); printf("child process id: %d\n", pid); printf("%d, g_global = %d\n", getpid(), g_global); } else { g_global = 10; printf("parent process id: %d\n", getppid()); printf("%d, g_global = %d\n", getpid(), g_global); } return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out` ```bash ☁ 3-process [master] ⚡ gcc main.c -o main.out ☁ 3-process [master] ⚡ ./main.out Hello World! current process id: 19135 parent process id: 19135 19136, g_global = 10 child process id: 19136 19135, g_global = 1 ``` **思考?** > 如何理解 “每个Linux进程可以执行一个或多个程序” ? **`execve(...)` 的工作方式** - 根据参数路径加载可执行程序 - 通过可执行程序信息构建进程数据,并写入当前进程空间 - 将程序执行位置重置到入口地址处(即:`main()`) - `execve()` 将重置当前进程空间(代码&数据)而不会创建新进程 > 下面的程序输出什么?为什么? ```C #define EXE "helloworld.out" int main() { char *args[] = {EXE, NULL}; printf("begin\n"); printf("pid = %d\n", getpid()); execve(EXE, args, NULL); printf("end\n"); return 0; } ``` > *[0-linux_basics/3-process/helloworld.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/3-process/helloworld.c)* ```C #include #include #include int main() { printf("execve = %d, %s\n", getpid(), "Hello World"); return 0; } ``` - 编译:`gcc helloworld.c -o helloworld.out` - 运行:`./helloworld.out` ```bash ☁ 3-process [master] ⚡ ./helloworld.out execve = 23724, Hello World ``` > *[0-linux_basics/3-process/test1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/3-process/test1.c)* ```C #include #include #include #define EXE "helloworld.out" int main() { char *args[] = {EXE, NULL}; printf("begin\n"); printf("pid = %d\n", getpid()); execve(EXE, args, NULL); printf("end\n"); return 0; } ``` - 编译:`gcc test1.c -o test1.out` - 运行:`./test1.out` ```bash ☁ 3-process [master] ⚡ ./test1.out begin pid = 25041 execve = 25041, Hello World ``` - 使用 `fork()` 创建子进程执行指定的程序代码 > *[0-linux_basics/3-process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/3-process/test.c)* ```C #include #include #include #define EXE "helloworld.out" int create_process(const char *path, char *const args[]) { int ret = fork(); if (ret == 0) { execve(path, args, NULL); } return ret; } int main() { char *args[] = {EXE, NULL}; printf("begin\n"); printf("pid = %d\n", getpid()); printf("child pid = %d\n", create_process(EXE, args)); printf("end\n"); return 0; } ``` - 编译:`gcc test.c -o test.out` - 运行:`./test.out` ```bash ☁ 3-process [master] ⚡ ./test.out begin pid = 25571 child pid = 25572 end execve = 25572, Hello World ``` ## 0-4. 进程参数编程 > **问题:** `execve(...)` 的参数分别是什么?有什么意义? ```C int create_process(char* path, char* args[]) { int ret = fork(); if (ret == 0) { execve(path, args, NULL); } return ret; } ``` **再论 `execve(...)`** ```C++ #include int execve(const char *pathname, char *const argv[], char *const envp[]); ``` **`main` 函数(默认进程入口)** - `int main(int argc, cahr *argv[])` - `argc` -- 命令行参数个数 - -----------------------> 启动参数 - `argv` -- 命令行参数数组 **进程空间概要** ![4-process_args1.png](image/4-process_args1.png) **进程参数存储分析** > *[0-linux_basics/4-process_args/meme.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/mem.c)* ```C++ #include #include #include #include static int g_init = 255; static float g_uninit; static void text() { } int main(int argc, char *argv[]) { static double s_init = 0.255; static double s_uninit; int i = 0; int *p = malloc(4); printf("argv[0] = %p\n", argv[0]); printf("&i = %p\n", &i); printf("p = %p\n", p); printf("&g_uninit = %p\n", &g_uninit); printf("&s_uninit = %p\n", &s_uninit); printf("&g_init = %p\n", &g_init); printf("&s_init = %p\n", &s_init); printf("text = %p\n", text); free(p); return 0; } ``` - 编译:`gcc mem.c -o mem.out` - 运行:`./mem.out` ```bash ☁ 4-process_args [master] ⚡ gcc mem.c -o mem.out ☁ 4-process_args [master] ⚡ ./mem.out argv[0] = 0x7ffdc2ba5efc &i = 0x7ffdc2ba3fac p = 0x557cafb2f2a0 &g_uninit = 0x557cae047028 &s_uninit = 0x557cae047030 &g_init = 0x557cae047010 &s_init = 0x557cae047018 text = 0x557cae044189 ``` - 下面的程序输出什么?为什么? > *[0-linux_basics/4-process_args/child.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/child.c)* ```C++ #include #include #include int main(int argc, char *argv[]) { int i = 0; sleep(3); for (i = 0; i < argc; ++i) { printf("exec = %d, %s\n", getpid(), argv[i]); } return 0; } ``` > *[0-linux_basics/4-process_args/parent.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/parent.c)* ```C++ #include #include #include #define EXE "child.out" int create_process(char *path, char *argv[]) { int ret = fork(); if (ret == 0) { execve(path, argv, NULL); } return ret; } void zero_str(char *s) { while (s && *s) { *s++ = 0; } } int main(int argc, char *argv[]) { char path[] = EXE; char arg1[] = "hello"; char arg2[] = "world"; char *args[] = {path, arg1, arg2, NULL}; printf("%d begin\n", getpid()); printf("%d child = %d\n", getpid(), create_process(EXE, args)); zero_str(path); zero_str(arg1); zero_str(arg2); printf("%d path = %s\n", getpid(), path); printf("%d arg1 = %s\n", getpid(), arg1); printf("%d arg2 = %s\n", getpid(), arg2); printf("%d end\n", getpid()); return 0; } ``` - 编译:`gcc child.c -o child.out` - 运行:`./child.out a b c` ```bash ☁ 4-process_args [master] ⚡ ./child.out a b c exec = 8897, ./child.out exec = 8897, a exec = 8897, b exec = 8897, c ``` - 编译:`gcc parent.c -o parent.out` - 运行:`./parent.out ` ```bash ☁ 4-process_args [master] ⚡ ./parent.out 9031 begin 9031 child = 9032 9031 path = 9031 arg1 = 9031 arg2 = 9031 end ☁ 4-process_args [master] ⚡ exec = 9032, child.out exec = 9032, hello exec = 9032, world ``` **Linux启动参数(命令行参数)规范** - 由选项,选项值,操作数组成 - 选项由短横线(`-`)开始,选项名必须是单个字母或数字字符 - 选项可以有选项值,选项与选项值之间可用空格分隔(`-o test` <==> `-otest`) - 如果多个选项均无选项值,可合而为一 (`-a -b -c` <==> `-abc`) - 既不是选项,也不能作为选项值的参数是操作数 (例如:`gcc test.c -o test.out`,`-o` 是选项,`test.out` 是 `-o` 的选项值,`test.c` 是操作数) - 第一次出现的双横线(`--`)用于结束所有选项,后续参数为操作数 **Linux启动参数(命令行参数)解析** - 规则:`if:s` `-i -s -f + 选项值` - 示例: | 命令行 | -f | -s | -i | parameter | err | | ---------------------- | --- | -- | -- | --------- | --- | | ./demo -f abc def | abc | x | x | def | | | ./demo -s -i -v | x | o | o | | -v | | ./demo abc -f gg de -s | gg | o | x | abc de | | | ./demo abc -- -f -s | x | x | x | abc -f -s | | | ./demo -s abc -i -f | x | o | o | abc | -f | **Linux启动参数(命令行参数)编程** ```C #include extern char* optarg; extern int optind, opterr, optopt; int getopt(int argc, char *const argv[], const char *optstring); ``` > `getopt(...)` 从 `argv[]` 数组中获取启动参数,并根据 `optstring` 定义的规则进行解析 - `getopt(...)` 从 `argc` 和 `argv` 中获取下一个选项 - 选项合法:返回值为选项字符,`optarg` 指向选项值字符串 > 例如:`gcc -c test.c -o test.o` `getopt(...)` 首先返回 `c`;`optarg` 指向 `test.c`,`getopt(...)` 返回的第二个选项为 `o`, `optarg` 指向 `test.o` > - 选项不合法:返回字符 `?`,`optopt` 保存当前选项字符(错误) - 选项合法但缺少选项值:返回字符 `:`, `optopt` 保存当前选项字符(错误) - 默认情况下:`getopt(...)` 对 `argv` 进行重排,所有操作数位于最后位置 > 例如:执行 `gcc test.c -o test.out` 命令时,`getopt(...)` 会对其进行重排,`getopt(...)` 先处理 `-o test.o` 再处理 `test.c`,因为 `test.c` 是操作数位于最后位置 > > *[0-linux_basics/4-process_args/main0.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/main0.c)* ```C #include #include #include int main(int argc, char *argv[]) { int i = 0; int c = 0; int iflag = 0; int fflag = 0; int sflag = 0; char *fvalue = NULL; while ((c = getopt(argc, argv, "f:is")) != -1) // -i -s -f 选项值 { switch (c) { case 'f': fflag = 1, fvalue = optarg; break; case 'i': iflag = 1; break; case 's': sflag = 1; break; case '?': printf("Unknow option: -%c\n", optopt); break; case ':': printf("-%c missing option argument\n", optopt); break; default: printf("ret = %d\n", c); } } printf("fflag = %d, fvalue = %s, iflag = %d, sflag = %d\n", fflag, fvalue, iflag, sflag); for (i = optind; i < argc; i++) { printf("parameter: %s\n", argv[i]); } return 0; } ``` - 编译:`gcc main0.c -omain0.out` - 运行:`./main0.out` ```bash ☁ 4-process_args [master] ⚡ ./main0.out fflag = 0, fvalue = (null), iflag = 0, sflag = 0 ``` - 运行:`./main0.out -i -s` ```bash ☁ 4-process_args [master] ⚡ ./main0.out -i -s fflag = 0, fvalue = (null), iflag = 1, sflag = 1 ``` - 运行:`./main0.out -is` ```bash ☁ 4-process_args [master] ⚡ ./main0.out -is fflag = 0, fvalue = (null), iflag = 1, sflag = 1 ``` - 运行:`./main0.out -si` ```bash ☁ 4-process_args [master] ⚡ ./main0.out -si fflag = 0, fvalue = (null), iflag = 1, sflag = 1 ``` > 运行:`./main0.out abc -f` ```bash ☁ 4-process_args [master] ⚡ ./main0.out abc -f ./main0.out: option requires an argument -- 'f' Unknow option: -f fflag = 0, fvalue = (null), iflag = 0, sflag = 0 parameter: abc ``` - 运行:`./main0.out -f aaa` ```bash ☁ 4-process_args [master] ⚡ ./main0.out -f aaa fflag = 1, fvalue = aaa, iflag = 0, sflag = 0 ``` - 运行:`./main0.out -v -m -n` ```bash ☁ 4-process_args [master] ⚡ ./main0.out -v -m -n ./main0.out: invalid option -- 'v' Unknow option: -v ./main0.out: invalid option -- 'm' Unknow option: -m ./main0.out: invalid option -- 'n' Unknow option: -n fflag = 0, fvalue = (null), iflag = 0, sflag = 0 ``` **`optstring` 规则的扩展定义** - 起始字符可以是 `:`, `+`, `-` 或省略 - 省略:出现错误选项时,程序中通过 `:` 或 `?` 进行处理并给出默认错误提示 - `:`:错误提示开关,程序中通过返回值 `:` 或 `?` 进行处理(无默认错误提示) - `+`:提前停止开关,遇见操作数时,返回 `-1`,认为选项处理完毕(后续都是操作数) - `-`:不重排开关,遇见操作数时,返回 `1`,`optarg` 指向操作数字符串 - 组合:`+:` or `-:` > *[0-linux_basics/4-process_args/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/main.c)* ```C #include #include #include int main(int argc, char *argv[]) { int i = 0; int c = 0; int iflag = 0; int fflag = 0; int sflag = 0; char *fvalue = NULL; while ((c = getopt(argc, argv, "-:f:is")) != -1) { switch (c) { case 'f': fflag = 1, fvalue = optarg; break; case 'i': iflag = 1; break; case 's': sflag = 1; break; case '?': printf("Unknow option: -%c\n", optopt); break; case ':': printf("-%c missing option argument\n", optopt); break; case 1: printf("inter: %s\n", optarg); break; default: printf("ret = %d\n", c); } } printf("fflag = %d, fvalue = %s, iflag = %d, sflag = %d\n", fflag, fvalue, iflag, sflag); for (i = optind; i < argc; i++) { printf("parameter: %s\n", argv[i]); } return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out -v -n -m` ```shell ☁ 4-process_args [master] ⚡ ./main.out -v -n -m Unknow option: -v Unknow option: -n Unknow option: -m fflag = 0, fvalue = (null), iflag = 0, sflag = 0 ``` - 修改 `main.c` ```C // ... while ((c = getopt(argc, argv, "+:f:is")) != -1) // ... ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out -v abc -n -m` ```shell ☁ 4-process_args [master] ⚡ ./main.out -v abc -n -m Unknow option: -v fflag = 0, fvalue = (null), iflag = 0, sflag = 0 parameter: abc parameter: -n parameter: -m ``` > 如上输出,使用规则 `+:f:is`, `-v` 为选项,`abc` 为操作数,遇到操作数时,返回 `-1`,认为选项处理完毕(后续都是操作数);即 `abc -n -m` 都是操作数 - 修改 `main.c` ```C // ... while ((c = getopt(argc, argv, "-:f:is")) != -1) // ... ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out -v abc -n -m` ```shell ☁ 4-process_args [master] ⚡ ./main.out -v abc -n -m Unknow option: -v inter: abc Unknow option: -n Unknow option: -m fflag = 0, fvalue = (null), iflag = 0, sflag = 0 ``` > 如上输出,使用规则 `-:f:is`, `-v` 为选项,`abc` 为操作数,遇到操作数时,返回 `1`,`optarg` 指向操作数字符串 `abc`; `-n -m` 为不合法的选项 ```shell ☁ 4-process_args [master] ⚡ ./main.out -v abc -n -m def Unknow option: -v inter: abc Unknow option: -n Unknow option: -m inter: def fflag = 0, fvalue = (null), iflag = 0, sflag = 0 ``` > 如上输出,使用规则 `-:f:is`, `-v` 为选项,`abc` 为操作数,遇到操作数时,返回 `1`,`optarg` 指向操作数字符串 `abc`; `-n -m` 为不合法的选项;`optarg` 指向操作数字符串 `def` **再论进程参数(短选项)** - 由选项,选项值,操作数组成 - 选项由短横线(`-`)开始,选项名必须是单个字母或数字字符 - 选项可以有选项值,选项与选项值之间可用空格分隔(`-o test` <--> `-otest`) - 如果多个选项均无选项值,可合而为一(`-a -b -c` <--> `-abc`) - 既不是选项,也不是能作为选项值的参数是操作数 - 第一次出现的双横线(`--`)用于结束所有选项,后续参数为操作数 **进程短选项示例** - 规则:`if:s::` ==> `s::` 表示 `-s` 不一定需要选项值,若有选项值只能以 `-svalue` 的方式指定 - 示例: | 命令行 | -f | -s | -i | parameter | err | | -------------------- | --- | --- | -- | --------- | --- | | ./demo -f abc def | abc | x | x | def | | | ./demo -s aaa -f bbb | bbb | o | x | aaa | | | ./demo -saaa -fbbb | bbb | aaa | x | | | | ./demo abc -- -f -s | x | x | x | abc -f -s | | | ./demo -s -f abc | abc | o | o | | | > *[0-linux_basics/4-process_args/main1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/main1.c)* ```C #include #include #include int main(int argc, char *argv[]) { int i = 0; int c = 0; int iflag = 0; int fflag = 0; int sflag = 0; char *fvalue = NULL; char *svalue = NULL; while ((c = getopt(argc, argv, "f:is::")) != -1) { switch (c) { case 'f': fflag = 1; fvalue = optarg; break; case 'i': iflag = 1; break; case 's': sflag = 1; svalue = optarg; break; case '?': printf("Unknow option: -%c\n", optopt); break; case ':': printf("-%c missing option argument\n", optopt); break; case 1: printf("inter: %s\n", optarg); break; default: printf("ret = %d\n", c); } } printf("fflag = %d, fvalue = %s, iflag = %d, sflag = %d, svalue = %s\n", fflag, fvalue, iflag, sflag, svalue); for (i = optind; i < argc; i++) { printf("parameter: %s\n", argv[i]); } return 0; } ``` - 编译:`gcc main1.c -o main1.out` - 运行:`./main1.out -s` ```shell ☁ 4-process_args [master] ⚡ ./main1.out -s fflag = 0, fvalue = (null), iflag = 0, sflag = 1, svalue = (null) ``` - 运行:`./main1.out -s abc` ```shell ☁ 4-process_args [master] ⚡ ./main1.out -s abc fflag = 0, fvalue = (null), iflag = 0, sflag = 1, svalue = (null) parameter: abc ``` - 运行:`./main1.out -sabc` ```shell ☁ 4-process_args [master] ⚡ ./main1.out -sabc fflag = 0, fvalue = (null), iflag = 0, sflag = 1, svalue = abc ``` **进程长参数编程** - 进程长参数示例 ``` ./demo.out --name=Fyang --addr 127.0.0.1 -xabc -- --test ------------ ---------------- ----- ------- ↓ ↓ ↙ ↙ 长选项 名:name 长选项 名:addr 短选项 操作数 值:Fyang 值:127.0.0.1 名:x 值:abc ``` **进程长参数规范(长选项)** - 由选项,选项值,操作数组成 - 选项由双横线(`--`)开始,选项名可以是有意义的字符序列 - 选项可以有选项值,选项与选项值可用空格/等号分隔(`--demo test <--> --demo=test`) - 既不是选项,也不能作为选项值的参数是操作数 - 第一次出现的双横线(`--`)用于结束所有选项,后续参数为操作数 **Linux 启动长参数(命令行参数)编程** ```C #include struct option { const char *name; int has_arg; int *flag; int val; }; extern char *optarg; extern int optind, opterr, optopt; int getopt_log(int argc, char* const argv[], const char* optstring, const struct option* longopts, int* longindex); ``` ```C struct option { const char *name; // 选项名 int has_arg; // 是否有选项值 int *flag; // 指向缩写字符 int val; // 缩写字符 }; int flag = -1; struct option long_options[] = { {"add", required_argument, &flag, 'a'}, {"delete", required_argument, &flag, 'd'}, {"create", required_argument, &flag, 'c'}, {"file", required_argument, &flag, 'f'}, {"del-all", required_argument, &flag, '0'}, {0, 0, 0, 0}, }; ``` - `getopt_long(...)` 从 `argc` 和 `argv` 中获取下一个选项 - `getopt_long(...)` 同时支持短选项和长选项 - 短选项:通过规则字符串指定支持的选项 - 长选项:通过 `struct option` 结构体指定支持的选项 - 默认情况下:`getopt_long(...)` 对 `agrv` 进行重排,所有操作数位于最后位置 **长参数编程示例** > *[0-linux_basics/4-process_args/long.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/long.c)* ```C #include #include int main(int argc, char *argv[]) { int c = 0; int option_index = 0; int flag = -1; struct option long_options[] = { {"add", required_argument, &flag, 'a'}, {"delete", required_argument, &flag, 'd'}, {"clear", no_argument, &flag, 'c'}, {0, 0, 0, 0}, }; while (1) { int c = getopt_long(argc, argv, "n:ml::", long_options, &option_index); if (c == -1) break; if (c) { if (c != '?') { printf("short: option: %c, value: %s\n", c, optarg); } } else { printf("long: option: %s, value: %s\n", long_options[option_index].name, optarg); } } if (optind < argc) { while (optind < argc) { printf("parameters: %s\n", argv[optind++]); } } return 0; } ``` - 编译:`gcc long.c -o long.out` - 运行命令:`./long.out --add a_test` ```shell ☁ 4-process_args [master] ⚡ ./long.out --add a_test long: option: add, value: a_test ``` - 运行命令:`./long.out -n n_test` ```shell ☁ 4-process_args [master] ⚡ ./long.out -n n_test short: option: n, value: n_test ``` - 运行命令:`./long.out --add a_test -n n_test` ```shell ☁ 4-process_args [master] ⚡ ./long.out --add a_test -n n_test long: option: add, value: a_test short: option: n, value: n_test ``` - 运行命令:`./long.out --test aaa` ```shell ☁ 4-process_args [master] ⚡ ./long.out --test aaa ./long.out: unrecognized option '--test' parameters: aaa ``` > *[0-linux_basics/4-process_args/long1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/long1.c)* ```C #include #include int main(int argc, char *argv[]) { int c = 0; int option_index = 0; int flag = -1; struct option long_options[] = { {"add", required_argument, &flag, 'a'}, {"delete", required_argument, &flag, 'd'}, {"clear", no_argument, &flag, 'c'}, {"list", optional_argument, &flag, 'l'}, {0, 0, 0, 0}, }; while (1) { int c = getopt_long(argc, argv, "n:ml::", long_options, &option_index); if (c == -1) break; printf("flag = %d(%c), c = %d(%c)\n", flag, flag, c, c); if (c) { if (c != '?') { printf("short: option: %c, value: %s\n", c, optarg); } } else { printf("long: option: %s, value: %s\n", long_options[option_index].name, optarg); } } if (optind < argc) { while (optind < argc) { printf("parameters: %s\n", argv[optind++]); } } return 0; } ``` - 编译:`gcc long1.c -o long1.out` - 运行命令:`./long1.out -ltest` ```shell ☁ 4-process_args [master] ⚡ ./long1.out -ltest flag = -1(�), c = 108(l) short: option: l, value: test ``` - 运行命令:`./long1.out --list` ```shell ☁ 4-process_args [master] ⚡ ./long1.out --list flag = 108(l), c = 0() long: option: list, value: (null) ``` - 运行命令:`./long1.out --list=test` ```shell ☁ 4-process_args [master] ⚡ ./long1.out --list=test flag = 108(l), c = 0() long: option: list, value: test ``` - 运行命令:`./long1.out --list test` ```shell ☁ 4-process_args [master] ⚡ ./long1.out --list test flag = 108(l), c = 0() long: option: list, value: (null) parameters: test ``` > 如上执行命令 `./long1.out --list=test` 和 `./long1.out --list test` 输出信息可知,`--list` 的选项值是可有可无的因此,`./long1.out --list test` 中的 `test` 为操作数;`--list` 的选项值必须使用 `=` 不能使用 ; **另一种长参数编程模式** ```C struct option long_options[] = { {"add", required_argument, 0, 'a'}, {"delete", required_argument, 0, 'd'}, {"clear", no_argument, 0, 'c'}, {0, 0, 0, 0 } }; while (1) { int c = getopt_long(argc, argv, "a:d:c", long_options, &option_index); if (c == -1) break; printf("short: option: %c, value: %s\n", c, optarg); } ``` > 不论长短参数,`getopt_long(...)` 均返回单个有效字符 > *[0-linux_basics/4-process_args/long-new.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/long-new.c)* ```C #include #include int main(int argc, char *argv[]) { int c = 0; int option_index = 0; int flag = -1; struct option long_options[] = { {"add", required_argument, 0, 'a'}, {"delete", required_argument, 0, 'd'}, {"clear", no_argument, 0, 'c'}, {"list", optional_argument, 0, 'l'}, {0, 0, 0, 0}, }; while (1) { int c = getopt_long(argc, argv, "a:d:cl::", long_options, &option_index); if (c == -1) break; printf("c = %c, optopt = %c, optarg = %s\n", c, optopt, optarg); } if (optind < argc) { while (optind < argc) { printf("parameters: %s\n", argv[optind++]); } } return 0; } ``` - 编译:`gcc long-new.c -o long-new.out` - 运行命令:`./long-new.out --add=a_test` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out --add=a_test c = a, optopt = , optarg = a_test ``` - 运行命令:`./long-new.out -a a_test` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out -a a_test c = a, optopt = , optarg = a_test ``` - 运行命令:`./long-new.out --list` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out --list c = l, optopt = , optarg = (null) ``` - 运行命令:`./long-new.out -l` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out -l c = l, optopt = , optarg = (null) ``` - 运行命令:`./long-new.out --list=LTest` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out --list=LTest c = l, optopt = , optarg = LTest ``` - 运行命令:`./long-new.out -lLTest` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out -lLTest c = l, optopt = , optarg = LTest ``` - 运行命令:`./long-new.out --test` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out --test ./long-new.out: unrecognized option '--test' c = ?, optopt = , optarg = (null) ``` - 运行命令:`./long-new.out -t` ```shell ☁ 4-process_args [master] ⚡ ./long-new.out -t ./long-new.out: invalid option -- 't' c = ?, optopt = t, optarg = (null) ``` **进程参数变种玩法 --> 统一长短参数** - `int getopt_long_only(argc, argv[], optstring, longopts, longindex);` - 对于以单横线(`-`)起始的参数 - 首先查找长参数选项,若查找失败,则查找短参数选项 - 对于以双横线(`--`)起始的参数 - 直接查找长参数选项,若查找失败,则查找失败 ```C struct option long_options[] = { {"test", required_argument, 0, 't'}, {"n", no_argument, 0, 'n'}, {0, 0, 0, 0 }, }; while (1) { int c = getopt_long_only(argc, argv, "d:f", long_options, &option_index); if (c == -1) break; printf("short: option: %c, value: %s\n", c, optarg); } ``` | 命令行 | test | n | d | f | parameter | | ---------------- | ---- | - | --- | - | --------- | | -test f abc | f | x | x | x | abc | | --test=f -n -f | f | o | x | o | | | -d dt -f | x | x | dt | o | | | abc -- -f -s | x | x | x | x | abc -f -s | | abc --n -ddef xy | x | o | def | x | abc xy | > `abc --n -ddef xy` 短选项写法 `-ddef` ==> `-d def` > *[0-linux_basics/4-process_args/long-only.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/4-process_args/long-only.c)* ```C #include #include int main(int argc, char *argv[]) { int c = 0; int option_index = 0; int flag = -1; struct option long_options[] = { {"test", required_argument, 0, 't'}, {"n", required_argument, 0, 'n'}, {0, 0, 0, 0}, }; while (1) { int c = getopt_long_only(argc, argv, "d:f", long_options, &option_index); if (c == -1) break; printf("c = %c, optopt = %c, optarg = %s\n", c, c, optarg); } if (optind < argc) { while (optind < argc) { printf("parameters: %s\n", argv[optind++]); } } return 0; } ``` - 编译:`gcc long-only.c -o long-only.out` - 运行命令:`./long-only.out -test` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out -test ./long-only.out: option '-test' requires an argument c = ?, optopt = ?, optarg = (null) ``` - 运行命令:`./long-only.out -test abc` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out -test abc c = t, optopt = t, optarg = abc ``` - 运行命令:`./long-only.out -test=abc` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out -test=abc c = t, optopt = t, optarg = abc ``` - 运行命令:`./long-only.out -n xyz` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out -n xyz c = n, optopt = n, optarg = xyz ``` - 运行命令:`./long-only.out -d nml` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out -d nml c = d, optopt = d, optarg = nml ``` - 运行命令:`./long-only.out --d` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out --d ./long-only.out: unrecognized option '--d' c = ?, optopt = ?, optarg = (null) ``` - 运行命令:`./long-only.out --n abc` ```shell ☁ 4-process_args [master] ⚡ ./long-only.out --n abc c = n, optopt = n, optarg = abc ``` **进程参数编程总结** | | 短选项 | 长选项 | 备注 | | --------------------- | ------ | ------ | ------------------------ | | getopt(...) | o | x | | | getopt_long(...) | o | o | 短选项与长选项分开处理 | | getopt_long_only(...) | o | o | 短选项写法兼容长选项定义 | ## 0-5. 环境变量编程 > **问题:** 环境变量是什么?有什么意义? ```C int create_process(char* path, char* args[]) { int ret = fork(); if (ret == 0) { execve(path, args, NULL); } return ret; } ``` **main 函数(默认进程入口)** - `int main(int argc, char *argv[], char *env[])` - `argc` -- 命令行参数个数 - `argv[]` -- 命令行参数数组 - `env[]` -- 环境变量数组(最后一个元素为 `NULL`) **什么是环境变量?** - 环境变量是进程运行过程中可能用到的 “键值对” (`NAME=Value`) - 进程拥有一个环境表(environment list),环境表包含了环境变量 - 环境表用于记录系统中相对固定的共享信息(不特定于具体进程) - 进程之间的环境表相互独立(环境表可在父进程之间传递) **环境表的构成** ![5-env_var_1.png](image/5-env_var_1.png) **下面的程序输出什么?为什么?** > *[0-linux_basics/5-env_var/child.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/5-env_var/child.c)* ```C #include #include #include int main(int argc, char *argv[], char *env[]) { int i = 0; sleep(1); printf("process parameter:\n"); for (i = 0; i < argc; ++i) { printf("exec = %d, %s\n", getpid(), argv[i]); } printf("enviroment list:\n"); i = 0; while (env[i]) printf("exec = %d, %s\n", getpid(), env[i++]); return 0; } ``` > *[0-linux_basics/5-env_var/parent.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/5-env_var/parent.c)* ```C #include #include #include #define EXE "child.out" int create_process(char *path, char *argv[], char *env[]) { int ret = fork(); if (ret == 0) { execve(path, argv, env); } return ret; } int main(int argc, char *argv[]) { char path[] = EXE; char arg1[] = "hello"; char arg2[] = "world"; char *args[] = {path, arg1, arg2, NULL}; printf("%d begin\n", getpid()); printf("%d child\n", create_process(path, args, args)); printf("%d end\n", getpid()); return 0; } ``` - 编译:`gcc child.c -o child.out` - 编译:`gcc parent.c -o parent.out` - 运行:`./parent.out` ```bash 14451 begin 14452 child 14451 end $ process parameter: exec = 14452, child.out exec = 14452, hello exec = 14452, world enviroment list: exec = 14452, child.out exec = 14452, hello exec = 14452, world ``` - 运行:`./child.out a b c d` ```bash ☁ 5-env_var [master] ⚡ ./child.out a b c d process parameter: exec = 5588, ./child.out exec = 5588, a exec = 5588, b exec = 5588, c exec = 5588, d enviroment list: exec = 5588, USER=fyang ... exec = 5588, SHELL=/usr/bin/zsh ... ``` - 查看环境变量 `echo $USER` ```bash ☁ 5-env_var [master] ⚡ echo $USER fyang ``` - 查看环境变量 `echo $SHELL` ```bash ☁ 5-env_var [master] ⚡ echo $SHELL /usr/bin/zsh ``` **深入理解环境变量** - 对于进程来说,环境变量是一种特殊的参数 - 环境变量相对于启动参数较稳定(系统定义且各个进程共享) - 环境变量遵守固定规范(如:键值对,变量名大写) - 环境变量与启动参数存储于同一内存区域(私有) **环境变量读写接口** - 头文件:`#include ` - 读:`char* getenv(const char *name);` - 返回 `name` 环境变量的值,如果不存在,返回 `NULL` - 写:`int putenv(char* string);` - 设置/改变环境变量 `(NAME=Value)`, `string` 不能是栈上定义的字符串 - 环境表入口:`extern char **environ;` > 下面的程序输出什么?为什么? ```C++ printf("original:\n"); printf("%s=%s:\n", "TEAS1", getenv("TEST1")); printf("%s=%s:\n", "TEAS2", getenv("TEST2")); printf("%s=%s:\n", "TEAS3", getenv("TEST3")); putenv("TEST1"); putenv("TEST2=NEW-VALUE"); putenv("TEST3=CREATE NEW"); printf("changed:\n"); printf("%s=%s:\n", "TEAS1", getenv("TEST1")); printf("%s=%s:\n", "TEAS2", getenv("TEST2")); printf("%s=%s:\n", "TEAS3", getenv("TEST3")); ``` > *[0-linux_basics/5-env_var/env.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/5-env_var/env.c)* ```C++ #include #include #include int main(int argc, char *argv[]) { printf("original:\n"); printf("%s=%s:\n", "TEAS1", getenv("TEST1")); printf("%s=%s:\n", "TEAS2", getenv("TEST2")); printf("%s=%s:\n", "TEAS3", getenv("TEST3")); putenv("TEST1"); putenv("TEST2=NEW-VALUE"); putenv("TEST3=CREATE NEW"); printf("new:\n"); printf("%s=%s:\n", "TEAS1", getenv("TEST1")); printf("%s=%s:\n", "TEAS2", getenv("TEST2")); printf("%s=%s:\n", "TEAS3", getenv("TEST3")); extern char **environ; int i = 0; printf("changed:\n"); while (environ[i]) printf("exec = %d, %s\n", getpid(), environ[i++]); return 0; } ``` - 编译:`gcc env.c -o env.out` - 运行:`./env.out` ```bash ☁ 5-env_var [master] ⚡ ./env.out original: TEAS1=(null): TEAS2=(null): TEAS3=(null): new: TEAS1=(null): TEAS2=NEW-VALUE: TEAS3=CREATE NEW: changed: exec = 10952, USER=fyang ... exec = 10952, TEST2=NEW-VALUE exec = 10952, TEST3=CREATE NEW ``` - 执行命令:`echo $TEST2` ```shell ☁ 5-env_var [master] ⚡ echo $TEST2 ``` - 执行命令:`echo $TEST3` ```shell ☁ 5-env_var [master] ⚡ echo $TEST3 ``` > 如上命令执行输出,新增的环境变量 `TEST2` 和 `TEST3` 属于进程 `env.out`,因此当进程 `env.out` 运行结束后环境变量 `TEST2` 和 `TEST3` 就没了;环境表是进程私有的,进程在运行过程中改变的环境表只影响当前进程,不影响父进程 **编程练习** - 编写一个应用程序,通过命令行参数读写环境变量 - 选项定义: - `-a`: 无选项值,输出所有环境变量 - `-r`: 读环境变量,`-n` --> 环境变量名 - `-w`: 写环境变量,`-n` --> 环境变量名, `-v` --> 环境变量值 - `-t`: 环境变量读写测试,先写入指定环境变量,之后输出所有环境变量 > *[0-linux_basics/5-env_var/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/5-env_var/main.c)* ```C #include #include #include #include #include typedef int OptCall(const char *, const char *); typedef struct { const char opt; OptCall *handler; } CallHandler; static int A_Handler(const char *n, const char *v) { extern char **environ; int i = 0; while (environ[i]) { printf("%s\n", environ[i++]); } return 0; } static int R_Handler(const char *n, const char *v) { if (n) { printf("%s=%s\n", n, getenv(n)); } else { printf("Need environ NAME to read value...\n"); } return 0; } static int W_Handler(const char *n, const char *v) { int err = 1; if (n && v) { char *kv = malloc(strlen(n) + strlen(v) + 2); if (kv) { strcpy(kv, n); strcat(kv, "="); strcat(kv, v); err = putenv(kv); if (!err) { printf("New Environ: %s\n", kv); } else { printf("Error on writing new environ value...\n"); } } free(kv); } else { printf("Need environ NAME VALUE to write...\n"); } return err; } static int T_Handler(const char *n, const char *v) { return W_Handler(n, v) || A_Handler(n, v); } static const CallHandler g_handler[] = { {'a', A_Handler}, {'r', R_Handler}, {'w', W_Handler}, {'t', T_Handler}, }; static const int g_len = sizeof(g_handler) / sizeof(*g_handler); int main(int argc, char *argv[]) { int c = 0; char opt = 0; char *name = NULL; char *value = NULL; while ((c = getopt(argc, argv, "arwtn:v:")) != -1) { switch (c) { case 'a': case 'r': case 'w': case 't': opt = c; break; case 'n': name = optarg; break; case 'v': value = optarg; break; default: exit(-1); } } c = 0; for (c = 0; c < g_len; c++) { if (opt == g_handler[c].opt) { g_handler[c].handler(name, value); } } return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out -a` ```shell ☁ 5-env_var [master] ⚡ ./main.out -a USER=fyang ... ZDOTDIR=/home/fyang USER_ZDOTDIR=/home/fyang ``` - 运行:`./main.out -r -n PATH` ```shell 5-env_var [master] ⚡ ./main.out -r -n PATH PATH=/home/fyang/.vscode-server/bin/ea1445cc7016315d0f5728f8e8b12a45dc0a7286/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Program Files/Common Files/Oracle/Java/javapath:/mnt/c/Program Files (x86)/VMware/VMware Workstation/bin/:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files/Microsoft VS Code/bin:/mnt/c/Program Files (x86)/Intel/Intel(R) Management Engine Components/DAL:/mnt/c/Program Files/Intel/Intel(R) Management Engine Components/DAL:/mnt/c/Program Files/dotnet/:/mnt/c/Program Files/Git/cmd:/mnt/c/Qt/6.3.0/mingw_64/bin:/mnt/c/Qt/Tools/QtCreator/bin:/mnt/c/Program Files/Pandoc/:/mnt/c/Program Files/Java/jdk-11.0.17/bin:/mnt/c/HighTec/toolchains/tricore/v4.9.3.0-infineon-1.0/bin:/mnt/c/Program Files (x86)/Windows Kits/10/Windows Performance Toolkit/:/mnt/c/msys64/ucrt64/bin:/mnt/c/Program Files/CMake/bin:/mnt/c/Users/yang/.cargo/bin:/mnt/c/Users/yang/AppData/Local/Microsoft/WindowsApps:/mnt/c/Program Files/JetBrains/CLion 2022.1.1/bin:/mnt/c/Program Files/JetBrains/PyCharm Community Edition 2022.3.2/bin:/mnt/c/Program Files/JetBrains/IntelliJ IDEA Community Edition 2022.3.3/bin:/mnt/c/Program Files/JetBrains/RustRover 232.9921.46/bin:/mnt/c/Users/yang/.dotnet/tools ``` - 运行:`./main.out -r -n PWD` ```shell ☁ 5-env_var [master] ⚡ ./main.out -r -n PWD PWD=/home/fyang/wkspace/linux_go/5-env_var ``` - 运行:`./main.out -w -n TEST -v Fyang` ```shell ☁ 5-env_var [master] ⚡ ./main.out -w -n TEST -v Fyang New Environ: TEST=Fyang ``` - 运行:`./main.out -w -t -n TEST -v Fyang` ```shell ☁ 5-env_var [master] ⚡ ./main.out -w -t -n TEST -v Fyang New Environ: TEST=Fyang USER=fyang ... ZDOTDIR=/home/fyang USER_ZDOTDIR=/home/fyang ``` > **Tips:** 根据执行命令 `./main.out -w -t -n TEST -v Fyang` 输出信息可知,环境变量 `TEST` 写入成功,但并未读出;其原因是因为 `W_Handler(...)` 函数中申请的堆空间变量 `kv` ,执行完 `free(kv)` 后被释放了,删除代码中 `free(kv)` 重新进行编译测试,可发现输出信息中存在环境变量 `TEST=Fyang` ```shell ☁ 5-env_var [master] ⚡ ./main.out -w -t -n TEST -v Fyang New Environ: TEST=Fyang USER=fyang ... TEST=Fyang ``` ## 0-6. 深入 Linux 进程 > **问题:** 进程参数和环境变量对于进程意味着什么? **进程参数和环境变量的意义** - 一般情况下,子进程的创建是为了解决某个子问题 - 子进程解决问题需要父进程的 “数据输入” (进程参数&环境变量) - 设计原则: - 子进程启动时必然用到的参数使用进程参数传递 - 子进程解决问题可能用到的参数使用环境变量传递 ![6-process_1.png](./image/6-process_1.png) > **思考:** 子进程如何将结果“返回”父进程? ![6-process_2.png](./image/6-process_2.png) > *[0-linux_basics/6-process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/6-process/test.c)* ```C #include int main() { printf("Test: Hello World!\n"); return 19; } ``` - 编译:`gcc test.c` - 运行:`./a.out` ```bash Test: Hello World! ``` - 使用命令 `echo $?` 查看最近一个进程的返回结果 ```bash $ echo $? 19 ``` **深入理解父子进程** - 子进程的创建是为了并行的解决子问题(问题分解) - 父进程需要通过子进程的结果最终解决问题(并获取结果) ```C #include #include pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); ``` **进程等待系统接口** - `pid_t wait(int *status);` - 等待一个子进程完成,并返回子进程标识和状态信息 - 当有多个进程完成,随机挑选一个子进程返回 - `pid_t waitpid(pid_t pid, int *status, int options);` - 可等待特定的子进程或一组子进程 - 在子进程还未终止时,可通过 `options` 设置不必等待(直接返回) **进程退出系统接口** - 头文件:`#include ` - `void _exit(int status);` //系统调用,终止当前进程 - 头文件:`#include ` - `void exit(int status);` //库函数,先做资源清理,再通过系统调用终止进程 - `void abort(void);` //异常终止当前进程(通过产生 `SIGABRT` 信号终止) > 下面的的程序运行后会发生什么? > *[0-linux_basics/6-process/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/6-process/main.c)* ```C #include #include #include #include #include int main() { pid_t pid = 0; int a = 1; int b = 0; int status = 0; printf("parent = %d\n", getpid()); if ((pid = fork()) == 0) exit(-1); printf("child = %d\n", pid); if ((pid = fork()) == 0) abort(); printf("child = %d\n", pid); if ((pid = fork()) == 0) a = a / b, exit(1); printf("child = %d\n", pid); sleep(3); while ((pid = wait(&status)) > 0) { printf("child %d, status = %x\n", pid, status); } return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./a.out` ```bash ☁ 6-process [master] ⚡ ./main.out parent = 5687 child = 5688 child = 5689 child = 5690 child 5688, status = ff00 child 5689, status = 86 child 5690, status = 88 ``` **进程退出状态详解** ![6-process_3.png](./image/6-process_3.png) | 宏 | 描述 | | | ----------------- | ------------------------------------------ | ----------- | | WIFEXITD(stat) | 通过 stat 判断进程是否正常结束 | return/exit | | WIFSIGNALED(stat) | 通过 stat 判断进程是否因为信号而被终止 | abort | | WIFSTOPPED(stat) | 通过 stat 判断进程是否因为信号而被暂停执行 | | | WEXITSTATUS(stat) | 获取正常结束时的状态值 | | | WTERMSIG(stat) | 获取导致进程终止的信号值 | | | WSTOPSIG(stat) | 获取导致进程暂停的信号值 | | > *[0-linux_basics/6-process/main_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/6-process/main_1.c)* ```C #include #include #include #include #include int main() { pid_t pid = 0; int a = 1; int b = 0; int status = 0; printf("parent = %d\n", getpid()); if ((pid = fork()) == 0) exit(-1); printf("child = %d\n", pid); if ((pid = fork()) == 0) abort(); printf("child = %d\n", pid); if ((pid = fork()) == 0) a = a / b, exit(1); printf("child = %d\n", pid); sleep(3); while ((pid = wait(&status)) > 0) { if (WIFEXITED(status)) { printf("Normal -- child: %d, code: %d\n", pid, (char)WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Signaled -- child: %d, code: %d\n", pid, WTERMSIG(status)); } else { printf("Paused -- child: %d, code: %d\n", pid, WSTOPSIG(status)); } } return 0; } ``` - 编译:`gcc main_1.c -o main_1.out` - 运行:`./main_1.out` ```bash ☁ 6-process [master] ⚡ ./main_1.out parent = 7334 child = 7335 child = 7336 child = 7337 Normal -- child: 7335, code: -1 Signaled -- child: 7336, code: 6 Signaled -- child: 7337, code: 8 ``` **僵尸进程** - 修改主进程休眠 120s, 观察子进程 > *[0-linux_basics/6-process/main_2.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/6-process/main_2.c)* ```C #include #include #include #include #include int main() { pid_t pid = 0; int a = 1; int b = 0; int status = 0; printf("parent = %d\n", getpid()); if ((pid = fork()) == 0) exit(-1); printf("child = %d\n", pid); if ((pid = fork()) == 0) abort(); printf("child = %d\n", pid); if ((pid = fork()) == 0) a = a / b, exit(1); printf("child = %d\n", pid); sleep(120); while ((pid = wait(&status)) > 0) { if (WIFEXITED(status)) { printf("Normal -- child: %d, code: %d\n", pid, (char)WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Signaled -- child: %d, code: %d\n", pid, WTERMSIG(status)); } else { printf("Paused -- child: %d, code: %d\n", pid, WSTOPSIG(status)); } } return 0; } ``` - 编译:`gcc main_2.c` - 后台运行:`./a.out &` ```bash [1] 8550 parent = 8550 child = 8552 child = 8553 child = 8554 ``` > 打印进程树:`pstree -p -A -s 8550` ```bash $ pstree -p -A -s 8550 init(1)---init(17471)---init(17472)---sh(17473)---sh(17474)---sh(17479)---node(17532)---node(17561)---bash(30774)---a.out(8550)-+-a.out(8552) |-a.out(8553) `-a.out(8554) ``` > 查看进程状态:`ps ax` ```bash 8550 pts/7 S 0:00 ./a.out 8552 pts/7 Z 0:00 [a.out] 8553 pts/7 Z 0:00 [a.out] 8554 pts/7 Z 0:00 [a.out] 8631 pts/7 R+ 0:00 ps ax 8632 pts/7 S+ 0:00 grep --color=auto pts/7 ``` **僵尸进程(僵死状态)** - 理论上,进程 **退出/终止** 后应立即释放所有系统资源 - 然而,**为了给父进程提供一些重要信息**,子进程 **退出/终止** 所占的部分资源会暂留 - 当父进程收集这部分信息后(`wait/waitpid`),子进程所有资源被释放 - 父进程调用 `wait()` ,为子进程 “收尸” 处理并释放暂留资源 - 若父进程退出,`init/systemd` 为子进程 “收尸” 处理并释放暂留资源 **僵尸的危害** - 僵尸进程保留进程的 终止状态 和 资源的使用信息 - 进程为何退出,进程消耗多少CPU时间,进程最大内存驻留值,等 - 如果僵尸进程得不到回收,那么可能影响正常进程的创建 - 进程创建最重要的资源是内存和进程标识 - 僵尸进程的存在可看作一种类型的内存泄露 - 当系统僵尸进程过多,可能导致进程标识不足,无法创建新进程 **`wait()` 的局限** - 不能等待指定子进程,如果存在多个子进程,只能逐一等待完成 - 如果不存在终止的子进程,父进程只能阻塞等待 - 只针对终止的进程,无法发现暂停的进程 **`wait(...)` 升级版 `pid_t waitpid(pid_t pid, int *status, int options);`** - 返回值相同,终止子进程标识符 - 状态值意义相同,记录子进程终止信息 - 特殊之处: | pid | 意义 | | --------- | -------------------------------------------------------------- | | pid > 0 | 等待进程标识符为 pid 的进程 | | pid == 0 | 等待当前进程组中的任意子进程 | | pid == -1 | 等待任意子进程,即:`wait(&stat) <==> waitpid(-1, &stat, 0)` | | pid < -1 | 等待指定进程组中的任意子进程 | > 下面的程序运行后会发生什么? ```C if ( (pid = fork()) < 0) { printf("fork error\n"); } else if (pid == 0) { int i = 0; for (i = 0; i < 5; i++) { if ( (pid = fork()) == 0 ) { worker(getpid()); break; } } sleep(15); } else { printf("wait child = %d\n", pid); while (waitpid(pid, &status, 0) == pid) { printf("Normal - child: %d, status = %x\n", pid, status); } } ``` ![6-skill.png](./image/6-skill.png) > *[0-linux_basics/6-process/skill.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/6-process/skill.c)* ```C #include #include #include #include #include static void worker(pid_t pid) { printf("grand-child: %d\n", pid); sleep(150); } int main(int argc, char *argv[]) { pid_t pid = 0; int status = 0; printf("parent = %d\n", getpid()); if ((pid = fork()) < 0) { printf("fork error\n"); } else if (pid == 0) { int i = 0; for (i = 0; i < 5; i++) { if ((pid = fork()) == 0) { worker(getpid()); break; } } sleep(60); printf("child(%d) is over ...\n", getpid()); } else { printf("wait child = %d\n", pid); sleep(120); while (waitpid(pid, &status, 0) == pid) { printf("Normal -- child: %d, status = %x\n", pid, status); } } return 0; } ``` - 编译:`gcc skill.c -o skill.out` - 运行:`./skill.out &` ```shell $ ./skill.out & [1] 5027 $ parent = 5027 wait child = 5028 grand-child: 5029 grand-child: 5030 grand-child: 5031 grand-child: 5032 grand-child: 5033 ``` - 执行命令:`ps ax | grep skill.out` ```shell $ ps ax | grep skill.out 5027 pts/0 S 0:00 ./skill.out 5028 pts/0 S 0:00 ./skill.out 5029 pts/0 S 0:00 ./skill.out 5030 pts/0 S 0:00 ./skill.out 5031 pts/0 S 0:00 ./skill.out 5032 pts/0 S 0:00 ./skill.out 5033 pts/0 S 0:00 ./skill.out 5544 pts/0 S+ 0:00 grep --color=auto skill.out ``` - 执行命令:`pstree -p -A -s 5027` ```shell $ pstree -p -A -s 5027 systemd(1)---systemd(1156)---gnome-terminal-(4113)---bash(4145)---skill.out(5027)---skill.out(5028)-+-skill.out(5029) |-skill.out(5030) |-skill.out(5031) |-skill.out(5032) `-skill.out(5033) ``` - 等待子进程结束,执行命令:`pstree -p -A -s 5027` ```shell $ child(5028) is over ... $ pstree -p -A -s 5027 systemd(1)---systemd(1156)---gnome-terminal-(4113)---bash(4145)---skill.out(5027)---skill.out(5028) ``` - 执行命令:`ps ax | grep skill.out` ```shell $ ps ax | grep skill.out 5027 pts/0 S 0:00 ./skill.out 5028 pts/0 Z 0:00 [skill.out] 5029 pts/0 S 0:00 ./skill.out 5030 pts/0 S 0:00 ./skill.out 5031 pts/0 S 0:00 ./skill.out 5032 pts/0 S 0:00 ./skill.out 5033 pts/0 S 0:00 ./skill.out 5632 pts/0 S+ 0:00 grep --color=auto skill.out ``` - 执行命令:`ps ax | grep skill.out` ```shell $ pstree -p -A -s 1156 systemd(1)---systemd(1156)-+-(sd-pam)(1165) ... ... ... | |-skill.out(5029) |-skill.out(5030) |-skill.out(5031) |-skill.out(5032) |-skill.out(5033) |-... ``` > 如上输出信息可知:子进程 5028 为僵尸进程,生成的孙进程由 `init/systemd` 进程接管 **僵尸进程避坑指南** - 利用 `wait(...)` 返回判断是否继续等待子进程 - `while( (pid = wait(&status)) > 0) {...}` - 利用 `waitpid(...)` 及 `init/systemd` 回收子进程 - 通过两次 `fork()` 创建孙进程解决子问题 ## 0-7. 进程创建大盘点 **进程创建回顾** ```C int create_process(char *path, char *args[], char* env[]) { int ret = fork(); if (ret == 0) { execve(path, args, env); } return ret; } ``` > **问题:** 进程创建是否只能依赖于 `fork()` 和 `execve(...)` ? **再论进程创建** `fork()` 通过完整复制当前进程的方式创建新进程,`execve()` 根据参数覆盖进程数据(一个不留) ![7-create_process_1.png](./image/7-create_process_1.png) **pid_t vfork(void);** - `vfork()` 用于创建子进程,然而不会复制父进程空间中的数据 - `vfork()` 创建的子进程直接使用父进程空间(没有完整独立的进程空间) - `vfork()` 创建的子进程对数据(变量)的修改会直接反馈到父进程中 - `vfork()` 是为了 `execve()` 系统调用而设计 **下面的程序运行后会发生什么?** > *[0-linux_basics/7-create_process/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/main.c)* ```C #include #include #include #include #include #include int main(int argc, char *argv[]) { pid_t pid = 0; int var = 88; printf("parent = %d\n", getpid()); if ((pid = vfork()) < 0) { printf("vfork error\n"); } else if (pid == 0) { printf("pid = %d, var = %d\n", getpid(), var); var++; printf("pid = %d, var = %d\n", getpid(), var); return 0; /* destroy parent stack frame */ // _exit(0); } printf("parent = %d, var = %d\n", getpid(), var); return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out` ```shell ☁ 7-create_process [master] ⚡ ./main.out parent = 24854 pid = 24855, var = 88 pid = 24855, var = 89 parent = 24854, var = 1556593248 [1] 24854 segmentation fault ./main.out ``` > 如上父进程输出,的 `var = 1556593248` 为非法值,并且产生 `segmentation fault`;是因为当创建的子进程运行结束 `return 0;` 时将父进程的栈空间也释放了(`vfork()` 创建的子进程直接使用父进程空间(没有完整独立的进程空间));此处应将 `return 0` 改为 `_exit(0);` **vfork()深度分析** ![7-create_process_2.png](./image/7-create_process_2.png) **vfork()要点分析** - `vfork()` 成功后,父进程将等待子进程结束 - 子进程可以使用父进程的数据(堆,栈,全局) - 子进程可以从创建点调用其它函数,但不要从创建点返回 - 当子进程执行流程回到创建点/需要结束时,使用 `_exit(0)` 系统调用 - 如果使用 `return 0` 那么将破坏栈结构,导致后续父进程执行出错 > *[0-linux_basics/7-create_process/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/main.c)* ```C #include #include #include #include #include #include int main(int argc, char *argv[]) { pid_t pid = 0; int var = 88; printf("parent = %d\n", getpid()); if ((pid = vfork()) < 0) { printf("vfork error\n"); } else if (pid == 0) { printf("pid = %d, var = %d\n", getpid(), var); var++; printf("pid = %d, var = %d\n", getpid(), var); // return 0; /* destroy parent stack frame */ _exit(0); } printf("parent = %d, var = %d\n", getpid(), var); return 0; } ``` - 编译:`gcc main.c -o main.out` - 运行:`./main.out` ```shell ☁ 7-create_process [master] ⚡ ./main.out parent = 26073 pid = 26074, var = 88 pid = 26074, var = 89 parent = 26073, var = 89 ``` **fork()的现代优化** - `Copy-on-Write` 技术 - 多个任务访问同一资源,在写入操作修改资源时,复制资源的原始副本 - `fork()` 引入 `Copy-on-Write` 之后,父子进程共享相同的进程空间 - 当父进程或子进程的其中之一修改内存数据,则实时复制进程空间 - fork() + execve() <--> vfork() + execve() - 修改 `create_process(...)` 函数:增加参数 `wait` ,`wait = 1` 父进程创建子进程后等待子进程执行结束;`wait = 0` 父进程创建子进程后不等待子进程结束 > *[0-linux_basics/7-create_process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/test.c)* ```C #include #include #include #include #include int create_process(char *path, char * const args[], char * const env[], int wait) { int ret = fork(); if (ret == 0) { if (execve(path, args, env) == -1) { exit(-1); } } if (wait && ret) { waitpid(ret, &ret, 0); } return ret; } int main(int argc, char* argv[]) { char* target = argv[1]; char* const ps_argv[] = {target, NULL}; char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Fyang", NULL}; int result = 0; if (argc < 2) exit(-1); printf("current: %d\n", getpid()); result = create_process(target, ps_argv, ps_envp, 0); // result = system("pstree -A -p -s $$"); // result = system(target); printf("result = %d\n", result); return 0; } ``` > *[0-linux_basics/7-create_process/helloworld.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/helloworld.c)* ```C #include int main() { printf("Hello World!!!\n"); return 0; } ``` - 编译 `helloworld.c` :`gcc helloworld.c -o helloworld.out` - 编译 `test.c` :`gcc test.c -o test.out` - 运行:`./test.out ./helloworld.out` ```shell ☁ 7-create_process [master] ⚡ ./test.out ./helloworld.out current: 30136 result = 30137 Hello World!!! ``` > 如上输出信息:`Hello World!!!` 在父进程执行结束后才输出,说明父进程未等待子进程执行结束 - 修改 `create_process(...)` 函数的参数 `wait = 1` > *[0-linux_basics/7-create_process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/test.c)* ```C #include #include #include #include #include int create_process(char *path, char * const args[], char * const env[], int wait) { int ret = fork(); if (ret == 0) { if (execve(path, args, env) == -1) { exit(-1); } } if (wait && ret) { waitpid(ret, &ret, 0); } return ret; } int main(int argc, char* argv[]) { char* target = argv[1]; char* const ps_argv[] = {target, NULL}; char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Fyang", NULL}; int result = 0; if (argc < 2) exit(-1); printf("current: %d\n", getpid()); result = create_process(target, ps_argv, ps_envp, 1); // result = system("pstree -A -p -s $$"); // result = system(target); printf("result = %d\n", result); return 0; } ``` - 编译 `helloworld.c` :`gcc helloworld.c -o helloworld.out` - 编译 `test.c` :`gcc test.c -o test.out` - 运行:`./test.out ./helloworld.out` ```shell ☁ 7-create_process [master] ⚡ ./test.out ./helloworld.out current: 8603 Hello World!!! result = 0 ``` > 如上输出信息:说明父进程等待子进程执行结束 **`exec` 函数家族** ```C #include extern char **environ; int execl(const char* pathname, const char *arg, .../* (char *) NULL */); int execlp(const char* file, const char *arg, .../* (char *) NULL */); int execle(const char* pathname, const char *arg, .../* (char *) NULL, char *const envp[] */); int execv(const char* pathname, char *const argv[]); int execvp(const char* file, char *const argv[]); int execve(const char* file, char *const argv[], char* const envp[]); ``` - `-l` -- list ==> 函数参数列表指定进程参数 - `-p` -- path ==> PATH指定的环境变量路径中查找可执行程序 - `-e` -- env ==> 环境变量(父进程提供) - `-v` -- 数组的方式提供进程参数 | | 进程参数 | 自动搜索PATH | 使用当前环境变量 | | ------------ | -------- | ------------ | ---------------- | | `execl()` | 列表 | No | Yes | | `execlp()` | 列表 | Yes | Yes | | `execle()` | 列表 | No | No,提供环境变量 | | `execv()` | 数组 | No | Yes | | `execvp()` | 数组 | Yes | Yes | | `execve()` | 数组 | No | No,提供环境变量 | **进程创建函数** - `#include ` - `int system(const char *command);` - 参数 -- 程序名及进程参数(如:`patree -A -p -s $$`) - 返回值 -- 进程退出状态值 ![7-create_process_3.png](./image/7-create_process_3.png) > *[7-create_process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/7-create_process/test.c)* ```C #include #include #include #include #include int create_process(char *path, char * const args[], char * const env[], int wait) { int ret = fork(); if (ret == 0) { if (execve(path, args, env) == -1) { exit(-1); } } if (wait && ret) { waitpid(ret, &ret, 0); } return ret; } int main(int argc, char* argv[]) { char* target = argv[1]; char* const ps_argv[] = {target, NULL}; char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Fyang", NULL}; int result = 0; if (argc < 2) exit(-1); printf("current: %d\n", getpid()); // result = create_process(target, ps_argv, ps_envp, 1); result = system("pstree -A -p -s $$"); // result = system(target); printf("result = %d\n", result); return 0; } ``` - 编译 `test.c` :`gcc test.c -o test.out` - 运行:`./test.out ./helloworld.out` ```shell ☁ 7-create_process [master] ⚡ ./test.out ./helloworld.out current: 24637 systemd(1)---init-systemd(Ub(2)---SessionLeader(874)---Relay(876)(875)---sh(876)---sh(877)---sh(882)---node(886)---node(1075)---zsh(7696+ result = 0 ``` > *[0-linux_basics/7-create_process/test.sh](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/test.sh)* ```shell echo "Hello world from shell ..." a=1 b=1 c=$(($a + $b)) echo "c = $c" ``` > *[0-linux_basics/7-create_process/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/7-create_process/test.c)* ```C #include #include #include #include #include int create_process(char *path, char * const args[], char * const env[], int wait) { int ret = fork(); if (ret == 0) { if (execve(path, args, env) == -1) { exit(-1); } } if (wait && ret) { waitpid(ret, &ret, 0); } return ret; } int main(int argc, char* argv[]) { char* target = argv[1]; char* const ps_argv[] = {target, NULL}; char* const ps_envp[] = {"PATH=/bin:/usr/bin", "TEST=Fyang", NULL}; int result = 0; if (argc < 2) exit(-1); printf("current: %d\n", getpid()); // result = create_process(target, ps_argv, ps_envp, 1); // result = system("pstree -A -p -s $$"); result = system(target); printf("result = %d\n", result); return 0; } ``` - 编译 `test.c` :`gcc test.c -o test.out` - 运行:`./test.out ./helloworld.out` ```shell ☁ 7-create_process [master] ⚡ ./test.out ./helloworld.out current: 25630 Hello World!!! result = 0 ``` - 运行:`./test.out ./test.sh` ```shell ☁ 7-create_process [master] ⚡ ./test.out ./test.sh current: 25739 Hello world from shell ... c = 2 result = 0 ``` ## 0-8. Linux 终端与进程 > **有趣的问题:** Linux中的终端,控制台,TTY,PTY究竟是什么?它们与进程有什么关系? **历史回顾:控制台(Console)** - 控制台是一个直接控制设备的面板(属于设备的一部分) - 计算机设备的控制台:按键 & 指示灯(键盘 & 显示器) - 早期的电子计算机必然有一个控制台 **历史回顾:终端(Terminal)** - 终端是一台独立于计算机的机器,是能够用来和计算机进行交互的设备 - TTY -- 即:TeleType Writer 电传打字机,一种终端设备 **历史发展进程** - 电传打字机已经淘汰 - 计算机上的输入设备和显示设备从主机独立出来 - 控制台与终端的物理表现形式逐渐趋近 - 计算机开始支持多任务处理 > **控制台 VS 终端** > > - 控制台是计算机的基本组成部分 > - 终端是连接/使用计算机的附加设备 > - 计算机只有一个控制台,但可以有多个终端 **终端与进程** TTY 演变为Linux中的抽象概念,对于进程而言 TTY 是一种输入输出设备 ![0-8-terminal_process_1.png](./image/0-linux_basic/0-8-terminal_process_1.png) **各种终端类型** | 类型 | 说明 | | | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------ | | 虚拟终端(Virtual Terminal) | 将这一套键盘和显示器映射为6个终端设备 | `/dev/tty1~tty6` tty0 指代当前使用的终端 | | 串口终端(Serial Port Terminal) | 将连接到串口的外设看作终端设备 | `/dev/ttyS1, ...` | | 终端模拟器(Terminal Emulator) | 终端模拟程序/内核模拟模块 | Putty, MobaXterm, 内核模块,伪终端,... | | 伪终端 (Pseudo Terminal) | 运行在用户模式的终端模拟程序,分为主设备(pty master)和从设备(pty slave) | `dev/ptmx`, `/dev/pts/3`, ... | **内核终端模拟器** ![0-8-terminal_process_2.png](./image/0-linux_basic/0-8-terminal_process_2.png) **伪终端模型** ![0-8-terminal_process_3.png](./image/0-linux_basic/0-8-terminal_process_3.png) **伪终端(gnome-terminal)** ![0-8-terminal_process_4.png](./image/0-linux_basic/0-8-terminal_process_4.png) **伪终端程序设计原理** ![0-8-terminal_process_5.png](./image/0-linux_basic/0-8-terminal_process_5.png) **伪终端程序设计(master)** - 创建 PTY 主从设备:`master = poxix_openpt(O_RDWR);` - 获取主设备权限: - `grantpt(master);` // 获取设备使用权限 - `unlockpt(master);` // 解锁设备,为读写做准备 - 读写主设备 - `c = read(master, &rx, 1);` - `len = write(master, txbuf, strlen(txbuf));` ```c char rx = 0; char rxbuf[32] = {0}; char txbuf[sizeof(rxbuf) * 2] = {0}; int master = 0; int c = 0; int i = 0; msater = posix_openpt(O_RDWR); grantpt(master); unlockpt(master); ptintf("Slave: %s\n", ptsname(master)); while ( (c = read(master, &rx, 1)) == 1 ) { if (rx == '\r') { rxbuf[i] = 0; sprintf(txbuf, "%s\r", rxbuf); write(master, txbuf, strlen(txbuf)); i = 0; } else { rxbuf[i++] = rx; } } ``` > *[0-linux_basics/8-terminal_process/master.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/8-terminal_process/master.c)* **伪终端程序设计(slave)** - 打开 PTY 从设备:`slave = open(path_to_slave, O_RDWR);` - 读写从设备: - `write(slave, "Fyang\r", 6);` - `read(slave, buf, sizeof(buf) - 1);` ```c int slave = open(argv[1], O_RDWR); if (slave > -1) { char buf[128] = {0}; int len = 0; write(slave, "Fyang\r", 6); sleep(1); len = read(slave, buf, sizeof(buf) - 1); buf[len] = 0; printf("Read: %d\n", buf); } else { printf("open slave error...\n"); } ``` > *[0-linux_basics/8-terminal_process/slave.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/8-terminal_process/slave.c)* > **思考:** 终端必然与进程关联才有意义!那么,进程之间除了父子关系,是否还有其它关系? ## 0-9. Linux 进程层次分析 **Linux 进程组** - 每个进程都有一个进程组号 - 进程组:一个或多个进程的集合(集合中的进程并不孤立) - 进程组中的进程通常存在父子关系,兄的关系,或功能相近 - 进程组可方便进程管理(如:同时杀死多个进程,发送一个信号给多个进程) - 每个进程必定属于一个进程组,也只能属于一个进程组 - 进程除了有 PID 外,还有 PGID(唯一,但可变) - 每个进程组有一个进程组长,进程组长的 PID 和 PGID 相同 ```bash > ps -o pgid 19843 PGID 977 > kill -- -977 ``` - `pid_t getpgrp(void);` // 获取当前进程的组标识 - `pid_t getpgid(pid_t pid);` // 获取指定进程的组标识 - `int setpgid(pid_t pid, pid_t pgid);` // 设置进程的组标识 - pid == pgid,将 pid 指定的进程设为组长 - pid == 0,设置当前进程的组标识 - pgid == 0,则将 pid 作为组标识 **进程组示例程序** ```cpp int pid = 0; int i = 0; printf("current = %d, ppid = %d, gpid = %d\n", getpid(), getppid(), getpgrp()); while (i < 5) { if ( (pid == fork()) > 0) { printf("new: %d\n", pid); } else if (pid == 0) { sleep(1); printf("child = %d, ppid = %d, gpid = %d\n", getpid(), getppid(), getpgrp()); break; } else { printf("fork error ...\n"); } i++; } ``` > *[0-linux_basics/9-process/pgid_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/9-process_session/pgid_1.c)* **深入理解进程组** - 进程组长终止,进程组依然存在(进程组长仅用于创建新进程组) - 父进程创建子进程后立即通过 `setpgid()` 改变其组标志(PGID) - 同时,子进程也需要通过 `setpgid()` 改变自身组标识(PGID) - 子进程调用 `exec()` - 父进程无法通过 `setpgid()` 改变其组标识(PGID) - 只能自身通过 `setpgid()` 改变其组标识(PGID) **进程组标识设置技巧** ```c if ((pid = fork()) > 0) { int r = setpgid(pid, pid); printf("new: %d, r = %d\n", pid, r); } else if (pid == 0) { setpgid(pid, pid); sleep("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp()); } else { printf("fork error ... \n"); } ``` > *[0-linux_basics/9-process/pgid_2.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/9-process_session/pgid_2.c)* > *[0-linux_basics/9-process/pgid_3.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/9-process_session/pgid_3.c)* **Linux 会话(session)** - 用户通过终端登录系统后会产生一个会话 - 会话是一个或多个进程组的集合 - 每个会话有一个会话标识(SID) - 终端登录后的第一个进程为会话首进程,通常是一个 shell/bash - 对于会话首进程(session leader),其 PID 与 SID 相等 - 通常情况下,会话与一个终端(控制终端)相关联用于执行输入输出操作 - 会话首进程建立与控制终端的连接(会话首进程又叫控制进程) - 会话中的进程组可分为 - 前台进程组:可以接收控制终端中的输入,也可以输出数据到控制终端 - 后台进程组:所有进程后台运行,无法接收终端的输入,但可以输出数据到终端 > 每个会话最多只有1个前台进程组,可以有多个后台进程组 - 会话与终端的关系 ![0-9-process_session_1.png](./image/0-linux_basic/0-9-process_session_1.png) ```bash ~$ ps -axj | grep agetty 1929 1943 1942 1929 pts/0 1942 S+ 1000 0:00 grep --color=auto agetty ~$ which agetty /usr/sbin/agetty ~$ ls -l /usr/sbin/agetty -rwxr-xr-x 1 root root 56896 4月 9 2024 /usr/sbin/agetty ~$ which getty /usr/sbin/getty ~$ ls -l /usr/sbin/getty lrwxrwxrwx 1 root root 6 4月 9 2024 /usr/sbin/getty -> agetty ~$ ``` - 在Ubuntu中使用 `Ctrl+Alt+F6` 启动虚拟终端,然后使用 `Ctrl+Alt+F2` 切换回 GUI 界面 ```bash ~$ ps -axj | grep agetty 1 2003 2003 2003 tty6 2003 Ss+ 0 0:00 /sbin/agetty -o -p -- \u --noclear tty6 linux 1942 2018 2017 1942 pts/0 2017 S+ 1000 0:00 grep --color=auto agetty ~$ pstree -s -p -A 2003 systemd(1)---agetty(2003) ``` - `Ctrl+Alt+F6` 切换到虚拟终端,输入登录用户名,密码先不回车登录;再次 `Ctrl+Alt+F2` 切换回 GUI 界面,查看进程树 ```bash ~$ pstree -s -p -A 2003 systemd(1)---login(2003) ``` > 可以看到 `getty` 进程加载了 `login` 进程 ![0-9-process_session_2.png](./image/0-linux_basic/0-9-process_session_2.png) **问题:在终端中输入命令后,发生了什么?** - 当命令行(shell)运行命令后创建一个新的进程组 - 如果运行的命令中有多个子命令则创建多个进程(处于新建的进程组中) - 命令不带 `&` - shell 将新建的进程组设置为前台进程组,并将自己暂时设置为后台进程组 - 命令带 `&` - shell 将新建的进程组设置为后台进程组,自己依旧是前台进程组 **什么是终端进程组标识(TPGID)?** - 标识进程是否处于一个和终端相关的进程组中 - 前台进程组:`TPGID == PGID` - 后台进程组:`TPGID != PGID` - 若进程和任何终端无关:`TPGID == -1` > 通过比较 `TPGID` 与 `PGID` 可判断:一个进程是属于前台进程组,还是后台进程组;由于前台进程组可能改变, `TPGID` 用于标识当前的前台进程组。 **Linux 会话接口** - `#include ` - `pid_t getsid(pid_t pid);` // 获取指定进程的 SID, (PID == 0) ==> 当前进程为父进程 - `pit_t setsid(void);` // 调用进程不能是进程组长 - 创建新会话,`SID == PID`,调用进程成为会话首进程 - 创建新进程组,`PGID == PID`,调用进程成为进程组长 - 调用进程没有控制终端,若调用前关联了控制终端,调用后与控制终端断联 ```c int main(void) { int pid = 0; if ((pid = fork()) > 0) { printf("parrent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid())); printf("new: %d\n", pid); } else if (pid == 0) { setsid(); sleep(3); printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid())); } else { printf("fork error ... \n"); } sleep(60); return 0; } ``` ## 0-10. 守护进程深度分析 > **思考:** 代码中创建的会话,如何关联控制终端? - 新会话关联控制终端的方法 - 会话首进程成功打开终端设备(设备打开前处于空闲状态) 1. 关闭标准输入输出和标准错误输出 2. 将 `stdin` 关联到终端设备:`STDIN_FILENO` ==> 0 3. 将 `stdout` 关联到终端设备:`STDOUT_FILENO` ==> 1 4. 将 `stderr` 关联到终端设备:`STDERR_FILENO` ==> 2 - 一些相关推论 - 新会话关联控制终端后,会话中的所有进程生命期与控制终端相关 - 只有会话首进程能够关联控制终端(会话中的其它进程不行) - 进程的标准输入输出与标准错误输出可以进行重定向 - 由描述符0,1,2决定重定向的目标位置(按顺序打开设备) - 控制终端与进程的标准输入输出以及标准错误输出无直接关系 **一个大胆的想法...** ![10-daemon_1.png](./image/0-linux_basic/10-daemon_1.png) - 示例代码 ```c else if (pid == 0) { setsid(); sleep(90); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); i += open(argv[1], O_RDONLY); // 0 ==> STDIN i += open(argv[1], O_WRONLY); // 1 ==> STDOUT i += open(argv[1], O_RDWR); // 2 ==> STDERR printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid())); printf("i = %d\n", i); } ``` > *[0-linux_basics/10-daemon/master.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/10-daemon/master.c)* > *[0-linux_basics/10-daemon/session.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/10-daemon/session.c)* - 运行:`./master.out` ```bash ☁ 10-daemon [master] ⚡ ./master.out Slave: /dev/pts/4 ``` - 在新终端中运行:`./session.out /dev/pts/4 ` ```bash ☁ 10-daemon [master] ⚡ ./session.out /dev/pts/4 parrent = 25058, ppid = 21518, pgid = 25058, sid = 21518 new: 25059 ``` - `ps -axj | grep 25059` ```bash ☁ 10-daemon [master] ⚡ ps -axj | grep 25059 578 25059 25059 25059 ? -1 Ss 1000 0:00 ./session.out /dev/pts/4 21518 25380 25379 21518 pts/11 25379 S+ 1000 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox 25059 ``` - 查看运行 `./master.out` 程序的终端输出信息 ```bash ☁ 10-daemon [master] ⚡ ./master.out Slave: /dev/pts/4 child = 25059, ppid = 578, pgid = 25059, sid = 25059 i = 3 ``` - 关闭运行 `./session.out /dev/pts/4 ` 的终端;打开新终端查看 `session.out` 进程发现 `session.out` 并未结束; ```bash ☁ 10-daemon [master] ⚡ ps -axj | grep session.out 578 25059 25059 25059 pts/4 25059 Ss+ 1000 0:00 ./session.out /dev/pts/4 26328 26425 26424 26328 pts/5 26424 S+ 1000 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox session.out ``` > 如上输出可知 `./session.out` 进程为会话首进程并与伪终端 `pts/4` 关联,结束 `master.out` 进程后,`./session.out` 也结束 **什么是守护进程(Daemon)?** - 守护进程是系统中执行任务的后台进程 - 生命周期长,一旦启动,正常情况下不会终止(直到系统退出) - Linux 大多数服务器使用守护进程实现(守护进程名以后缀 `d` 结尾) **守护进程的创建步骤** 1. 通过 `fork()` 创建新进程,成功后,父进程退出 2. 子进程通过 `setsid()` 创建新会话 3. 子进程通过 `fork()` 创建孙进程(肯定不是会话首进程) 4. 孙进程修改模式 `umask()`,改变工作目录为 `/` 5. 关闭标准输入输出和标准错误输出 6. 重定向标准输入输出和标准错误输出(`/dev/null`) **守护进程的创建步骤** ![10-daemon_2.png](./image/0-linux_basic/10-daemon_2.png) **守护进程关键点分析** - 父进程创建子进程是为了创建新会话 - 子进程创建孙进程是为了避免产生控制进程 - 孙进程不是会话首进程,因此不能关联终端 - 重定向操作可以避开奇怪的进程输出行为 > *[0-linux_basics/10-daemon/first-d.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/10-daemon/first-d.c)* ## 0-11. 信号发送与处理(上) > **问题:** 按下 Ctrl + c 后,命令行中的前台进程会被终止。为什么? **什么是信号?** - 信号是一种“软件中断”,用来处理异步事件 - 内核发送信号到某个进程,通知进程事件的发生 - 事件可能来自硬件,可能来自用户输入,可能来自除零错误 - 信号是一种类型的进程间通信方式(一个进程向另一个进程发送信号) - A 进程发生事件 T ,向 B 进程发送信号,B 进程执行动作响应事件 - 进程可以对接收到的不同信号进行不同动作响应(信号-->处理) **信号的分类** - 硬件异常 - 内核检测到硬件错误,发送相应的信号给相关进程 - 终端信号 - 在终端输入“特殊字符”等价于向前台进程组发送相应信号 - 软件信号 - 在软件层面(进程代码中)触发的信号(发送给自身或其它进程) **硬件异常信号** | 信号 | 值 | 说明 | | ------- | -- | -------------------------------- | | SIGBUS | 7 | 总线错误,进程发生了内存访问错误 | | SIGFPE | 8 | 算术错误,FPE表示浮点异常 | | SIGILL | 9 | 指令错误,进程尝试执行非法指令 | | SIGSEGV | 11 | 段错误,进程访问了非法内存区域 | **终端相关信号** - SIGINT(Ctrl + c) - 程序终止信号,用于通知前后台进程终止进程 - SIGQUIT(Ctrl + \) - 与 SIGINT 类似,进程收到该信号退出时可产生 coredump 文件 - SIGTSTP(Ctrl + z) - 停止进程的运行,进程收到信号后可以选择处理或忽略 **软件相关信号** - 子进程退出:父进程收到 SIGCHLD 信号 - 父进程退出:子进程可能收到信号(什么信号?) - 定时器到期:`alarm()`, `ualarm()`, `timer_crate()`, ... - 主动发信号:`kill()`, `raise()`, ... - ... **内核与信号** ![0-11-signal_1.png](./image/0-linux_basic/0-11-signal_1.png) **信号的默认处理** | 默认处理方式 | 说明 | 示例 | | ------------- | ---------------------------- | ---------------- | | ignore | 进程丢弃信号不会产生任何影响 | SIGCHLD,SIGURG | | terminate | 终止进程 | SIGKILL,SIGHUP | | coredump | 终止进程并产生转存储文件 | SIGQUIT,SIGILL | | stop/continue | 停止进程执行/恢复进程执行 | SIGSTOP,SIGCONT | **System V vs. BSD** - System V - 也被称为 AT&T SystemV, 是 Unix 操作系统众多版本中的一支 - BSD - 加州大学伯克利分校开创,Unix 衍生系统,代表由此派生出的各种套件集合 > Linux 之所以被称为类 Unix 操作系统(Unix Like),部分原因是 Linux 的操作风格是介于上述二者之间,且不同厂商为了照顾不同的用户,其发行版的操作风格存在差异 | | SystemV | BSD | | ------------------ | ----------- | --------- | | root 脚本设置 | /etc/init.d | /etc/rc.d | | 默认 shell | Bshell | Cshell | | 文件系统数据 | /etc/mnttab | /etc/mtab | | 内核位置 | /UNIX | /vmUnix | | 打印机设备 | lp | rlp | | 字符串函数 | memcopy | bcopy | | 终端初始化设置文件 | /etc/initab | /etc/ttys | | 终端控制 | termio | termios | **自定义信号处理** ```c #include #include typedef void(*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); sighandler_t sysv_signal(int signum, sighandler_t handler); sighandler_t bsd_signal(int signum, sighandler_t handler); ``` **信号处理示例** ```c #include void signal_handler(int sig) { printf("sig = %d\n", sig); } int main(int argc, char* argv[]) { int i = 0; signal(SIGINT, signal_handler); // sysv_signal(SIGINT, signal_handler); // bsd_signal(SIGINT, signal_handler); while(1) { sleep(1); } return 0; } ``` > *[0-linux_basics/11-signal/main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/11-signal/main.c)* **自定义信号发送** ```c #include #include int kill(pid_t pid, int sig); // pid > 0, pid == 0, pid < -1 int raise(int sig); // 信号处理完毕才返回 ``` > 标准信号是 Unix 系统中的信号,编号范围从 1 到 31 > 实时信号是 Linx 独有的信号,编号范围从 32 到 64 **信号发送示例** ```c int main(int argc, char* argv[]) { int pid = atoi(argv[1]); int sig = atoi(atgv[2]); printf("send sig(%d) to process(%d)\n", sig, pid); kill(pid, sig); rasie(SIGINT); while(1) { printf("while...\n"); slepp(1); } return 0; } ``` > *[0-linux_basics/11-signal/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/11-signal/test.c)* ## 0-12. 信号发送与处理(下) > **问题:** 三种注册信号与处理函数的方法有什么区别??? **信号的 `OneShot` 特性** - SystemV 风格的 `signal` 函数,注册的信号处理是一次性的 - 进程收到信号后,调用由 `signal` 注册的处理函数 - 处理函数一旦执行,进程通过默认的方式处理后续相同信号 - 如果想要重复触发,那么必须再次调用 `signal` 注册的处理函数 > BSD 风格的 `signal` 函数不存在 `OneShot`,能够自动反复触发处理函数的调用 **信号的自身屏蔽特性** - 在信号处理函数执行期间,很可能再次收到当前信号 - 即:处理 A 信号的时候,再次收到 A 信号 - 对于 SystemV 风格的 signal 函数,会引起信号处理函数的重入 - 即:调用处理函数的过程中,再次触发处理函数的调用 - 在注册信号处理函数时: - SystemV 风格的 signal 不屏蔽任何信号 - BSD 风格的 signal 会屏蔽当前注册的信号 > BSD 风格的 signal 函数,处理 A 信号期间,如果收到 B 信号会发生什么? **系统调用的重启特性** - 系统调用期间,可能收到信号,此时进程必须从系统调用中返回 - 对于执行时间较长的系统调用(`write/read`),被信号中断的可能性很大 - 如果希望信号处理之后,被中断的系统调用能够重启,则:可以通过条件 `errno == EINTR` 判断重启系统调用 **系统调用重启示例代码** ```c pid_t r_wait(int* status) { int ret = -1; while (status && ((ret = wait(status)) == -1) && (errno == EINTR) ); return ret; } ``` - SystemV 风格的 signal 函数 - 系统调用被信号中断后,直接返回 `-1`,并且 `errno == EINTR` - BSD 风格的 signal 函数 - 系统调用被中断,内核在信号处理函数结束后,自动重启系统调用 **注意事项** - 并非所有的系统调用对信号中断都表现同样的行为 - 一些信号支持信号中断后自动重启 - `read()`, `write()`, `wait()`, `waitpid()`, `ioctl()`, ... - 一些信号完全不支持中断后自动重启 - `poll()`, `select()`, `usleep()`, ... **三种方法的区别** | 函数 | OneShot | 屏蔽自身 | 重启系统调用 | | ---------------- | ------- | -------- | ------------ | | signal(...) | false | true | true | | sysv_signal(...) | yes | false | false | | bsd_signal(...) | false | true | true | > 在信号处理上,Linux 系统更新接近 BSD 风格的操作;默认的 signal 函数在不同的 Linux 发行版上语义可能不同,从代码移植角度,避免直接使用 `signal(...)` 函数 **现代信号处理注册函数** ```c #include int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact); ``` **现代信号处理语义分析** ```c struct sigaction { void (*sa_handler)(int); // 信号处理函数 void (*sa_sigaction)(int, siginfo_t*, void*); // 信号处理函数 sigset_t sa_mask; // 处理期间屏蔽信号集 int sa_flags; // 信号处理特性标志位 void (*sa_restorer)(void); // 废弃不再使用 }; ``` **信号屏蔽与标记** - `sigset_t sa_mask` - 信号屏蔽:`sa_mask = SIGHUP | SIGINT | SIGUSR1;` - 注意:并不是所有信号都可以被屏蔽,如:`SIGKILL`, `SIGTOP` - `int sa_flags` - 信号特性:`sa_flag = SA_ONESHOT | SA_RESTART;` - 特殊性:`(SA_SIGINFO)`,信号处理时能够收到额外的附加信息 **信号状态小知识** - 信号产生 - 信号来源,如:`SI_KERNEL`, `SI_USER`, `SI_TIMER`, ... - 信号未决 - 信号产生到信号被进程接收的状态(处于未决状态的信号必然已存在) - 信号递达 - 信号送达进程,被进程接收(忽略,默认处理,自定义处理) **信号屏蔽 vs 信号阻塞** - 信号屏蔽 - 信号处理函数执行期间,被屏蔽的信号不会被递送给进程(针对多个信号) - `sa_mask = SIGHUP | SIGINT | SIGUSR1` - 信号阻塞 - 信号处理函数执行期间,当前信号不会递送给进程(当前信号) - `act.sa_flags = SA_RESTART | SA_NODEFER;` **现代信号处理注册示例** ```c int main() { char buf[32] = {0}; struct sigaction act = {0}; act.sa_handler = delay_handler; act.sa_flags = SA_RESTART | SA_NOOEFER; sigaddset(&act.sa_mask, 40); sigaddset(&act.sa_mask, SIGINT); sigaction(40, &act, NULL); sigaction(SIGINT, &act, NULL); r_read(buf, 32); printf("input: %s\n", buf); return 0; } ``` > *[0-linux_basics/12-signal/main_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/12-signal/main_1.c)* **现代信号发送** ```c #include int sigqueue(pid_t pid, int sig, const union sigval value); // 信号附带数据(4字节) ``` ```c union sigval { int sival_int; void* sival_ptr; // 对于 Linux 进程,指针成员无意义 }; ``` - `sigqueue(...)` 的黄金搭档是 `sigaction(...)` - `sa_flags` 设置 `SA_SIGINFO` 标志位,可使用三参数信号处理函数 ```c void handler(int sig, siginfo_t* info, void* ucontext) { // ... } ``` > 类型为 `ucontext_t*` 指针,用于描述执行信号处理函数之前的进程上下文信息;极少数情况会使用该参数 **现代信号处理函数的关键参数** ```c siginfo_t { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */ pid_t si_pid; /* Sending process ID */ uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_value; /* System time consumed */ sigval_t si_val; /* Signal value */ // ... } ``` **现代信号发送处理示例** > *[0-linux_basics/12-signal/main_2.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/12-signal/main_2.c)* > *[0-linux_basics/12-signal/test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/12-signal/test.c)* ## 0-13. 信号可靠性剖析 > **问题:** 基于信号发送的进程间通信方式可靠吗??? **信号查看(kill -l)** ```bash fyang@fyang-vm:~$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX ``` **信号的分类** - 不可靠信号(传统信号) - 信号值在 [1, 31] 之间的所有信号 - 可靠信号(实时信号) - 信号值在 `[SIGRTMIN, SIGRTMAX]`, 即:`[34, 64]` - SIGRTMIN -> 34 - SIGRTMAX -> 64 - 信号 32 与 信号 33 (SIGCANCEL & SIGSETXID)被 NPTL 线程库征用 - NTPL -> Native Posix Threading Library - 即:POSIX 线程标准库,Linux 可以使用这个库进行多线程编程 - 对于 Linux 内核,信号 32 是最小的可靠信号 - SIGRTMIN 在 `signal.h` 中定义,不同平台的 Linux 可能不同(arm linux) **不可靠信号 vs 可靠信号** - 不可靠信号 - 内核不保证信号可以递送到目标进程(内核对信号状态进行标记) - 如果信号处于未决状态,并且相同信号被发送,内核丢弃后续相同信号 - 可靠信号 - 内核维护信号队列,未决信号位于队列中,因此信号不会被丢弃 - 严格意义上,信号队列有上限,因此不能无限制保存可靠信号 **注意事项** - 不可靠信号的默认处理行为可能不同(忽略,结束) - 可靠信号的默认处理行为都是结束进程 - 信号的可靠性由信号数值决定,与发送方式无关 - 信号队列的上限可通过命令设置 - 查询信号队列上限:`ulimit -i` - 设置信号队列上限:`ulimit -i 10000` **信号可靠性试验设计** - 目标:验证信号可靠性(不可靠信号 or 可靠信号) - 方案:对目标进程 “疯狂” 发送 N 次信号,验证信号处理函数调用次数 - 预备函数: - `int sigaddset(sigset_t* set, int signum);` - `int sigdelset(sigset_t* set, int signum);` - `int sigfillset(sigset_t* set);` - `int sigemptyset(sigset_t* set);` - `int sigprocmask(int how, const sigset_t* set; sigset_t* oldset);` ```c printf("current pid(%d)...\n", getpid()); g_obj_sig = atoi(argv[1]); act.sa_sigaction = signal_handler; act.sa_flags = SA_RESTART | SA_SIGINFO; sigaddset(&act.sa_mask, g_obj_sig); sigaction(g_obj_sig, &act, NULL); // 屏蔽所有信号,即:发送给进程的信号,无法递达,处于未决状态 sigfillset(&set); sigprocmask(SIG_SETMASK, &set, NULL); for (int i = 0; i < 15; i++) { sleep(1); printf("i = %d\n", i); } // 解除信号屏蔽,未决状态的信号,将递达进程 sigemptyset(&set); sigprocmask(SIG_SETMASK, &set, NULL); printf("g_count = %d\n", g_count); ``` > *[main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/13-signal/main.c)* > *[test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/13-signal/test.c)* **基于信号的进程间通信实验** - A 进程将 TLV 类型的数据通过可靠信号传递给 B 进程 - TLV --> (type, length, value) - 由于可靠信号的限制,每次传输4字节数据 - B 进程首先接收 4 个字节数据(type 或 type + length) - 根据接收到 length 信息,多次接收后续的字节数据 - 每次只能接收 4 字节数据,设计层面需要进行状态处理 **状态设计** ![0-13-signal.png](./image/0-linux_basic/0-13-signal.png) **数据发送进程关键实现** ```c typedef struct { short type; short length; char data[]; } Message; ``` ```c union sigval sv = {0}; int *pi = (int *)msg; printf("current pid(%d)...\n", getpid()); printf("send sig(%d) to process(%d)...\n", sig, pid); msg->type = 0; msg->length = len - sizeof(Message); strcpy(msg->data, data); for (int i = 0; i < size; i += 4) { sv.sival_int = *pi++; sigqueue(pid, sig, sv); } ``` > *[test_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/13-signal/test_1.c)* **数据接收进程关键实现** ```c int data = info->si_value.sival_int; if( g_current == -1 ) { g_type = data & 0xFFFF; g_length = (data >> 16) & 0xFFFF; g_current = 0; g_data = malloc(g_length); if( !g_data ) { exit(-1); } } else { int i = 0; while( (i < 4) && (g_current < g_length) ) { g_data[g_current] = (data >> (i * 8)) & 0xFF; g_current++; i++; } } if( g_current == g_length ) { ipc_data_handler(g_data, g_length); g_type = -1; g_length = -1; g_current = -1; free(g_data); } ``` > *[main_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/13-signal/main_1.c)* ## 0-14. 信号优先级与安全性 > **问题:** 对于同一个进程,如果存在两个不同的未决实时信号,那么先处理谁??? **信号优先级的概念** - 信号的本质是一种软中断(中断有优先级,信号也有优先级) - 对于同一个未决实时信号,按照发送先后顺序递送给进程 - 对于不同的未决实时信号,信号值越小优先级越高 - 不可靠信号与可靠信号同时未决: - 严格意义上,没有明确规定优先级 - 实际上,Linux 优先递送不可靠信号 **信号优先级的概念** - 多个不可靠信号同时未决,优先递送谁? - 优先递送硬件相关信号 - `SIGSEGV`, `SIGBUS`, `SIGILL`, `SIGTRAP`, `SIGFPE`, `SIGSYS` - 优先递送信号值小的不可靠信号 - 不可靠信号优先于可靠信号递送 **信号优先级实验设计** - 目标:验证信号的优先级 - 场景:不可靠 vs 不可靠;不可靠 vs 可靠;可靠 vs 可靠 - 方案:对目标进程发送 N 次 “无” 序信号,验证信号递达进程的先后次序 - 预备函数: - `int sigaddset(sigset_t* set, int signum);` - `int sigfillset(sigset_t* set);` - `int sigemptyset(sigset_t* set);` - `int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);` - 需要思考的问题 - 如何使得多个信号同时未决,且以优先级方式递达进程? - 如何记录和对比信号的递达次序及发送次序? - 对于实验中涉及的不可靠信号,是否特殊考虑? - ... **信号优先级实验设计(发送端)** ```c for (i = 0; i < num; i++) { int sig = 0; do { sig = rand() % 64 + 1; // 随机数产生信号 } while ( find(special, slen, sig) ); printf("send sig(%d) to process(%d)...\n", sig, pid); sv.sival_int = i + 1; // 发送次序 sigqueue(pid, sig, sv); } ``` > *[test.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/14-signal/test.c)* **信号优先级实验设计(接收端)** ```c for (int i = 1; i <= 64; i++) { sigaddset(&act.sa_mask, i); } for (int i = 1; i < 64; i++) { sigaction(i, &act, NULL); } sigfillset(&set); sigprocmask(SIG_SETMASK, &set, NULL); for (int i = 0; i < 15; i++) { sleep(1); printf("i = %d\n", i); } sigemptyset(&set); sigprocmask(SIG_SETMASK, &set, NULL); ``` ```c typedef struct _sig_info_ { int sig; int index; } SigInfo; static int g_index = 0; static SigInfo g_sig_arr[80] = {0}; // 优先级高的信号被记录在较小下标的数组元素中 void signal_handler(int sig, siginfo_t* info, void* ucontext) { g_sig_arr[g_index].sig = info->si_signo; g_sig_arr[g_index].index = info->si_value.sival_int; g_index++; } ``` > *[main.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/14-signal/main.c)* **再论信号处理** ![0-14-signal_1.png](./image/0-linux_basic/0-14-signal_1.png) > 信号递达进程,改变的仅仅是执行流,而执行上下文没有任何改变 **信号安全性** - 什么是信号安全性? - 程序能正确且无意外的按照预期方式执行 - 信号处理的不确定性 - 什么时候信号递达是不确定的 -> 主程序被中断的位置是不确定的 - 当信号递达,转而执行信号处理函数时,不可重入的函数不能调用 - 不可重入函数:函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断) **下面的程序输出什么?为什么?** > *[sigex.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/14-signal/sigex.c)* ```c #include #include #include #include #include #include static int g_current = -1; static void add_func(int n) { g_current = 0; for (int i = 1; i <= n; i++) { g_current += i; sleep(1); } } static void signal_handler(int sig, siginfo_t *info, void *ucontext) { write(STDOUT_FILENO, "handler begin\n", 14); add_func(5); write(STDOUT_FILENO, "handler end\n", 12); } int main(int argc, char *argv[]) { struct sigaction act = {0}; int obj_sig = atoi(argv[1]); printf("current pid(%d) ...\n", getpid()); act.sa_sigaction = signal_handler; act.sa_flags = SA_RESTART | SA_NODEFER; sigaction(obj_sig, &act, NULL); add_func(10); printf("g_current = %d\n", g_current); return 0; } ``` - 程序输出: ```bash ☁ 14-signal [master] ⚡ ./a.out 2 current pid(17265) ... g_current = 55 ☁ 14-signal [master] ⚡ ./a.out 2 current pid(17372) ... ^Chandler begin handler end g_current = 67 ``` > add_func 为不可重入函数,由上两次输出可知,信号处理函数中调用不可重入函数会导致错误的值 **深入信号安全性** - 不要在信号处理函数中调用不可重入的函数(即:使用了全局变量的函数) - 不要在信号处理函数中存在临界区的函数(可能产生竞争导致死锁) - 不要调用 `malloc()` 和 `free()` 函数 - 不要调用标准 I/O 函数,如:`printf()` 函数 - ... > 小问题:如何知道哪些函数是安全的? - `man 7 signal-safety` ## 0-15. 信号处理设计模式 > **思考:** 如何编写信号安全的应用程序??? **Linux 应用程序安全性讨论** - 场景一:不需要处理信号 - 应用程序实现单一功能,不需要关注信号 - 如:数据处理程序,文件加密程序,科学计算程序 - 场景二:需要处理信号 - 应用程序长时间运行,需要关注信号,并及时处理 - 如:服务端程序,上位机程序 **场景一:不需要信号处理(单一功能应用程序)** - 信号处理回避模式 > 在代码层面,直接阻塞所有可能的信号!!! ```c static void mask_all_signal() { sigset_t set = {0}; sigfillset(&set); sigprocmask(SIG_SETMASK, &set, NULL); } int main(int argc, char* argv[]) { int i = 0; mask_all_signal(); printf("current pid(%d)...\n", getpid()); while (i < g_jlen) { int argc = g_job[i].argc; char* argv = g_job[i].argv; g_job[i].job_func(argc, argv); i++; } return 0; } ``` > *[15-signal/main_0.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/15-signal/main_0.c)* **场景二:需要处理信号(长时间运行的应用)** - 同步方案 - 通过标记同步处理信号,整个应用只有一个执行流 - 异步方案 - 专用任务处理,应用中存在多个执行流(多线程应用) - 设置专用信号处理任务,其它任务忽略信号,专注功能实现 - 同步解决方案(单任务) - 信号处理逻辑与程序逻辑位于同一个上下文 - 即:信号处理函数与主函数不存在资源竞争关系 - 方案设计一 - 将任务分解为子任务(每个任务可对应一个函数) - 信号递达时,信号处理函数中仅标记递达状态 - 子任务处理结束后,正真执行信号处理 **同步方案示例一** ```c static siginfo_t g_siga_arr[65] = {0}; static const g_slen = sizeof(g_sig_arr) / sizeof(*g_sig_arr); static void signal_handler(int sig, siginfo_t *info, void* uncontext) { g_sig_arr[sig] = *info; g_sig_arr[0].si_signo++; // 只做标记,不具体处理信号 } int main(int argc, char* argv[]) { int i = 0; printf("current pid(%d)...\n", getpid()); app_init(); while (i < g_jlen) { int argc = g_job[i].argc; char *argv = g_job[i].argv; g_job[i].job_func(argc, argv); process_signal(); // 根据标记,执行信号处理逻辑 i++; } return 0; } ``` > *[15-signal/main_1.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/15-signal/main_1.c)* **存在的问题** > 由于给每个信号唯一的标记位置,因此,所有有信号转变为不可靠信号;并且仅保留最近递达的信号信息。 > **可能的改进方案:** 标记位置设置为链表,信号递达后在对应位置的链表处增加节点保留信号信息。 **同步解决方案(单任务)** - 方案设计二 - 将任务分解为子任务(每个任务可对应一个函数) - 创建信号文件描述符,并阻塞所有信号(可靠信号递达前位于内核队列中) - 子任务处理结束后,通过 select 机制判断是否有信号需要处理 - `true` --> 处理信号 `false` --> 等待超时 **关键系统函数** ```c #include #include int signalfd(int fd, const sigset_t* mask, int flags); int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); ``` **使用 `signalfd()` 处理信号** > 先屏蔽所有信号(无法递达进程),之后为屏蔽信号创建文件描述符;当时机成熟,通过 `read()` 系统调用读取未决信号(主动接收信号) ```c sigset_t set = {0}; sigfillset(&set); sigprocmask(SIG_SETMASK, &set, NULL); g_sig_fd = signalfd(-1, &set, 0); ``` ```c struct signalfd_siginfo si = {0}; if (read(g_sig_fd, &si, sizeof(si)) > 0) { do_sig_process(&si); } ``` **使用 `select` 监听文件描述符** ```c fd_set reads = {0}; fd_set temps = {0}; struct timeval timemot = {0}; FD_ZERO(&reads); FD_SET(0, &reads); while (1) { int r = -1; temps = reads; timeout.tv_sec = 0; timeout.tv_usec = 5000; r = select(1, &temps, 0, 0, &timeout); if ( r > 0 ) { if ( FD_ISSET(0, &temps) ) { // read data from console } } else if ( r == 0 ) { usleep(10000); } else { break; } } ``` **使用 `select()` 处理信号** ```c static int max = 0; fd_set reads = {0}; fd_set rset = {0}; struct timeval timeout = {0}; FD_ZERO(&reads); FD_SET(g_sig_fd, &reads); max = g_sig_fd; rset = reads; timeout.tv_sec = 0; timeout.tv_usec = 5000; while ( select(max+1, &rset, 0, 0, &timeout) > 0 ) { max = select_handler(&rset, &reads, max); } ``` > *[15-signal/main_2.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/15-signal/main_2.c)* > **存在的问题:** 由于使用了 select 机制,即便没有信号需要处理,也需要等待 select 超时,任务实时性受到影响 > **思考:** 是否可以兼顾信号处理与任务执行的实时性? **异步解决方案(多任务)** - 使用独立任务处理信号,程序逻辑在其它任务中执行 - 即:通过多线程分离信号处理与程序逻辑 - 主线程:专用于处理信号 - 其它线程:完成程序功能 > 问题:信号递达进程后,在哪一个执行流中进行处理? **多线程信号处理** - 信号的发送目标是进程,而不是某个特定的线程 - 发送给进程的信号仅递送给一个线程 - 内核从不会阻塞目标信号的线程中随机选择 - 每个线程拥有独立的信号屏蔽掩码 - 主线程:对目标信号设置信号处理的方式 - 当信号递达进程时,只可能是主线程进行信号处理 - 其它线程:首先屏蔽所有可能的信号,之后执行任务代码 - 无法接收到信号,不具备信号处理能力 **进程与线程** - 进程:应用程序的一次加载执行(系统进行资源分配的基本单位) - 线程:进程中的程序执行流 - 一个进程中可以存在多个线程(至少存在一个线程) - 每个线程执行不同的任务(多个线程可并行执行) - 同一个进程中的多个线程共享进程的系统资源 **Linux 多线程API函数** - 头文件:`#include ` - 线程创建函数:`int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);` - `thread:` `pthread_t` 变量的地址,用于返回线程标识 - `attr:` 线程的属性,可设置为 `NULL`, 即:使用默认属性 - `start_routine:` 线程入口函数 - `agr:` 线程入口函数参数 - 线程标识 - `pthread_t pthread_self(void);` - 获取当前线程的 ID 标识 - 线程等待: - `int pthread_join(pthread_t thread, void** retval);` - 等待目标线程执行结束 **多线程编程示例** ```c void* thread_entry(void* arg) { pthread_t id = pthread_self(); int n = (long)arg; int i = 0; while (i < n) { printf("id = %ld, i = %d\n", id, i); sleep(1); i++; } return NULL; } int main() { pthread_t t1 = 0; pthread_t t3 = 0; int arg1 = 5; int arg2 = 10; pthread_create(&t1, NULL, thread_entry, (void*)arg1); pthread_create(&t1, NULL, thread_entry, (void*)arg2); printf("t1 = %ld\n", t1); printf("t2 = %ld\n", t2); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0; } ``` **异步方案示例--主线程** ```c static void app_init() { struct sigaction act = {0}; sigset_t set = {0}; act.sa_sigaction = signal_handler; act.sa_flag = SA_RESTART | SA_SIGINFO; for (int i = 1; i <= 64; i++) { sigaddset(&act.sa_mask, i); } for (int i = 1; i <= 64; i++) { sigaction(i, &act, NULL); } } int main(int argc, char* argv) { pthread_t tid = 0; printtf("current pid(%d)...\n", getpid()); app_init(); pthread_create(&tid, NULL, thread_entry, NULL); pthread_join(tid, NULL); mask_all_signal(); printf("app end\n"); return 0; } ``` **异步方案示例--任务线程** ```c static void* thread_entry(void* arg) { int i = 0; mask_all_signal(); while (i < g_jlen) { int argc = g_job[i].argc; char* argv = g_job[i].argv; g_job[i].job_func(argc, argv); i++; } } ``` > *[15-signal/main_3.c](https://gitee.com/fyang0906/linux_go/blob/master/0-linux_basics/15-signal/main_3.c)* **信号设计模式小结** - 多数程序不需要处理信号,因此可直接屏蔽信号 - 需要处理信号的程序,重点考虑信号安全性问题 - 同步处理方案,通过设计让任务代码和信号处理代码交替执行 - 问题:信号处理是否及时?任务执行是否实时? - 异步处理方案,任务代码与信号处理代码位于不同执行流 - 问题:将信号安全性问题转换为线程安全性问题;因此,程序本身是否做到线程安全? ## 1-1. 初探 Linux 进程调度 > **已知:** 父进程创建子进程后,父子进程同时运行 > > **问题:** 如果计算机只有一个处理器,父子进程以什么方式同时执行? **Linux系统调度** - 内核具有进程调度的能力,多个进程可以同时运行 - 微观上,处理器同一时间只能执行一个进程 - 同时运行多个进程时,每个进程都会获得适当的执行时间片 - 当执行时间片用完,内核调度下一个进程执行 **进程调度原理** - n 个进程(n >= 2)同时位于内存中 - 处理器执行每个进程,每个进程拥有一个时间片 - 时间片用完,通过时钟中断完成进程切换(调度) ```c void Schedule() { gCTaskAddr = &gTaskBuff[index % 4]; index++; PrepareForRun(gTaskAddr); LoadTask(gCTaskAddr); } ``` **Linux系统调度策略** - 普通调度策略 **SCHED_OTHER:** Linux默认的调度策略,也被称为CFS(Completely Fair Scheduler),给每个进程动态计算优先级,根据优先级和进程执行的历史记录来确定下一个执行的进程。 - 实时调度策略 **SCHED_FIFO:** 基于优先级顺序调度进程,并在一个进程获得CPU时一直执行,直到进程主动释放。 **SCHED_RR:** 基于“时间片轮转”的调度策略,给每个进程设置一个固定的时间片,并按照优先级顺序对进程进行轮流调度。 **如何验证Linux中的进程调度?** - 实验目标: - 验证同一时刻只有一个进程在执行 - 验证不同调度策略,进程执行的连续性不同 - 实验设计: - n 个进程同时运行,统计各个进程的执行时刻 - 进程运行方式: - 每个 slice 时间记录如下值:进程编号,当前时间值,完成度 - 在 total 时间后结束运行,并输出记录的数据 - 通过记录的数据分析进程调度策略 **实验中需要解决的问题** - 如何让进程每次“固定”工作 slice 时间(单位毫秒)? - 如何获取和改变进程的调度策略? - 如何记录数据并输出数据(需要保存数据)? - 如何图形化显示数据? **Linux中的时间获取** ```c #include struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; int clock_gettime(clockid_t clk_id, struct timespec* tp); ``` - `clk_id:` - `CLOCK_MONOTONIC` --> 系统启动后到当前的时间 - `CLOCK_REALTIME` --> 1970.1.1 到当前的时间 **“固定”时间工作量估算** ```c #define NLOOP_FOR_ESTIMATION 1000000000UL #define NSECS_FER_MSEC 1000000UL #define NSECS_PER_SEC 1000000000UL #define DiffNs(begin, end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC\ + (end.tv_nsec - begin.tv_nsec)) static unsigned long estimate_loops_per_msec() { struct timespec begin = {0}; struct timespec end = {0}; unsigned long i = 0; clock_gettime(CLOCK_MONOTONIC, &begin); while (i < NLOOP_FOR_ESTIMATION) i++; clock_gettime(CLOCK_MONOTONIC, &end); return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / DiffNs(begin, end); } ``` **获取/改变进程调度策略** ```c #include struct sched_param { // ... int shced_priority; // ... }; int sched_setscheduler(pid_t pid, int policy, const struct sched_param* param); int sched_getscheduler(pid_t pid); ``` **获取/改变进程调度策略** - chrt 命令简介 - Linux 系统中可以使用 chrt 命令来查看、设置一个进程的优先级和调度策略 - 命令用法 - `chrt [options] [prio] [pid | command [arg]...]` - 主要参数 - `-p`, `--pid` 操作一个已存在的PID,不启动一个新的任务 - `-f`, `--fifo` 设置调度策略为 `SCHED_FIFO` - `-m`, `--max` 显示最小和最大有效优先级,然后退出 - `-o`, `--other` 设置调度策略为 `SCHED_OTHER` - `-r`, `--rr` 设置调度策略为 `SCHED_RR` - 示例 - 指定目的进程的PID来更改调度策略 - `chrt -p -r 99 1328` - 更改bash为实时进程,优先级为10 - `chrt -f 10 bash` **记录进程运行后产生数据** ```c fd = open(buf, O_WRONLY | O_CREAT | O_TRUNC); if (fd != -1) { for (int i = 0; i < nrecord; i++) { sprintf(buf, "%d\t%ld\t%d\n", id, DiffNs(g_time_begin, tss[i]) / NSECS_PER_MSEC, (i + 1) * 100 / nrecord); write(fd, buf, strlen(buf)); } } close(fd); ``` **进程调度数据收集** - `lscpu` 查看当前计算机处理器状态 ```bash ☁ 1-process_scheduling [master] ⚡ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 94 Model name: Intel(R) Core(TM) i5-6300HQ CPU @ 2.30GHz Stepping: 3 CPU MHz: 2303.999 BogoMIPS: 4607.99 Virtualization: VT-x Hypervisor vendor: Microsoft ``` - `taskset` 指定程序在某一个CPU上执行 > *helloworld.c* - 编译 `helloworld.c` ```bash ☁ 1-process_scheduling [master] ⚡ gcc helloworld.c -o helloworld.out ``` - 后台运行 `helloworld.out` ```bash ☁ 1-process_scheduling [master] ⚡ ./helloworld.out & [2] 20560 child = 20560, ppid = 10990, pgid = 20560 hello world ``` - `taskset -p pid` 查看当前进程状态信息 ```bash ☁ 1-process_scheduling [master] ⚡ taskset -p 20560 pid 20560's current affinity mask: f ``` > 如上输出信息说明,当前进程能够在任意一个cpu上运行,由操作系统调度决定 - `taskset -c 0 ./helloworld.out &` 指定 helloworld.out 在0号CPU上运行 ```bash ☁ 1-process_scheduling [master] ⚡ taskset -c 0 ./helloworld.out & [2] 22013 child = 22013, ppid = 10990, pgid = 22013 hello world ``` - 查看 helloworld.out 是否在0号CPU上运行 ```bash ☁ 1-process_scheduling [master] ⚡ taskset -p 22013 pid 22013's current affinity mask: 1 ``` > 如上输出信息说明 helloworld.out 在0号CPU上运行 > *main.c* - 编译 main.c 使其在CPU 0上运行,创建3个进程,每个进程执行1000ms,时间片(slice)的值为5ms ```bash ☁ 1-process_scheduling [master] ⚡ taskset -c 0 ./main.out 3 1000 5 nproc = 3 total = 1000 slice = 5 SCHED_OTHER = 0 SCHED_FIFO = 1 SCHED_RR = 2 estimatting the workload for one slice... g_load_per_slice = 2429005 task 2 ==> schedule policy: 0 task 2 ==> schedule priority: 0 task 1 ==> schedule policy: 0 task 1 ==> schedule priority: 0 task 0 ==> schedule policy: 0 task 0 ==> schedule priority: 0 ./2-proc.log ./0-proc.log ./1-proc.log ``` **进程调度数据分析** - 系统调度:SCHED_OTHER - `python3 proc_graph.py id-time 3` ![1-process_scheduling_1.png](./image/1-process_thread/1-process_scheduling_1.png) ![1-process_scheduling_2.png](./image/1-process_thread/1-process_scheduling_2.png) ## 1-2. 多核调度预备知识 > **问题:** 内核对进程调度时发生了什么? **进程调度的本质** - 任务/进程 切换 - 即:上下文切换,内核对处理器上执行的进程进行切换 - “上下文”指:寄存器的值 - “上下文切换”指: - 将寄存器的值保存到内存中(进程被剥夺处理器,停止执行) - 将另一组寄存器的值从内存中加载到寄存器(调度下一个进程执行) > 当时间片耗完,不管进程正在执行什么代码,都一定会发生上下文切换! ![2-multicore_scheduling_1.png](./image/1-process_thread/2-multicore_scheduling_1.png) - 上下文切换必然导致进程状态的转换 - 上下文切换由中断触发(时钟中断,IO中断,等) > **有趣的问题:** 上下文切换时,突然收到一个中断会发生什么?? ![2-multicore_scheduling_2.png](./image/1-process_thread/2-multicore_scheduling_2.png) **详解Linux进程状态(ps au)** | 状态符 | 对应的状态 | 描述 | | - | - | - | | R | TASK_RUNNING | 可执行状态&执行状态 | | S | TASK_INTERRUPTIBLE | 可中断的睡眠状态(等待事件发生) | | D | TASK_UNINTERRUPTIBLE | 不可中断的睡眠状态(IO等待) | | T | TASK_STOPPED | 暂停状态(信号暂停) | | t | TASK_TRACED | 跟踪状态(调试器暂停) | | Z | TASK_DEAD | 退出状态(僵死进程) | | X | TASK_DEAD | 退出状态 | | 状态符 | 描述 | | - | - | | < | 进程优先级比较高 | | N | 进程优先级比较低 | | L | 进程有页面被锁定在内存中 | | I | 内核空闲线程 | | s | 会话首进程 | | l | 进程有多少个线程 | | + | 进程在前台进程组中 | > *[helloworld.c](https://gitee.com/fyang0906/linux_go/blob/master/1-process_thread/2-multicore_scheduling/helloworld.c)* - 编译 `helloworld.c` ```bash gcc helloworld.c -o helloworld.out -lpthread ``` - 运行 `helloworld.out` ```bash $ ./helloworld.out pid = 4539, ppid = 3515, pgid = 4539 hello world! ``` - 在新终端中查看进程运行状态 ```bash $ ps au USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND fyang 1132 0.0 0.1 165520 6400 tty2 Ssl+ 09:27 0:00 /usr/libexec/gdm-x-session -- fyang 1136 0.8 2.3 366560 93268 tty2 Sl+ 09:27 0:10 /usr/lib/xorg/Xorg vt2 -displ fyang 1264 0.0 0.4 226408 16512 tty2 Sl+ 09:27 0:00 /usr/libexec/gnome-session-bi fyang 2953 0.0 0.1 14364 5632 pts/0 Ss+ 09:29 0:00 bash fyang 3515 0.0 0.1 14788 5888 pts/1 Ss 09:33 0:00 /bin/bash --init-file /home/f fyang 4539 176 0.0 10972 1408 pts/1 Rl+ 09:48 2:00 ./helloworld.out fyang 4566 0.2 0.1 14664 6016 pts/2 Ss 09:48 0:00 /bin/bash --init-file /home/f fyang 4629 0.0 0.0 15760 3584 pts/2 R+ 09:49 0:00 ps au ``` > 从上述信息可看到 `helloworld.out` 进程的状态为 `Rl+`,说明 `helloworld.out` 处于运行状态,是一个多线程进程,并且处于前台进程组中 - 使 `helloworld.out` 在 CPU0 上执行:`taskset -c 0 ./helloworld.out` ```bash $ taskset -c 0 ./helloworld.out pid = 5350, ppid = 3515, pgid = 5350 hello world! ``` - 查看 `helloworld.out` 进程信息 ```bash USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND fyang 1132 0.0 0.1 165520 6400 tty2 Ssl+ 09:27 0:00 /usr/libexec/gdm-x-session --run-script env G fyang 1136 0.5 2.3 366560 93268 tty2 Sl+ 09:27 0:10 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /ru fyang 1264 0.0 0.4 226408 16512 tty2 Sl+ 09:27 0:00 /usr/libexec/gnome-session-binary --session=u fyang 2953 0.0 0.1 14364 5632 pts/0 Ss+ 09:29 0:00 bash fyang 3515 0.0 0.1 14788 5888 pts/1 Ss 09:33 0:00 /bin/bash --init-file /home/fyang/.vscode-ser fyang 4566 0.0 0.1 14664 6016 pts/2 Ss 09:48 0:00 /bin/bash --init-file /home/fyang/.vscode-ser fyang 5350 99.6 0.0 10972 1408 pts/1 Rl+ 10:00 1:37 ./helloworld.out fyang 5414 0.0 0.0 15760 3584 pts/2 R+ 10:01 0:00 ps au ``` > 从上述信息可以看出 `helloworld.out` 进程CPU占用率为 99.6%,不会超过 100%,所以Linux的基本调度单位是线程,不是进程 **细说空闲状态** - 处理器上电后,开始一直不停的向下执行指令 - 当系统中没有进程时,会执行一个 “不执行任何操作” 的空闲进程 - 空闲进程的职责:执行特殊指令使处理器进入休眠状态(低功耗状态) - 空闲状态是一种暂态,但凡出现就绪态进程,空闲状态立即结束 **Linux性能工具简介** - `ps` -- 查看进程运行时数据(`ps au`) - `top` -- Linux 整体性能监测工具(类似任务管理) - `sar` -- Linux 活动情况报告(系统性能分析工具) **Linux系统平均负载** - 即:Linux 系统负载平均值(Linux System Load Averages) - 该值表示的是一段时间内任务对系统资源需求的平均值(1, 5 和 15 分钟) - 如果平均值接近0,意味着系统处于空闲状态 - 如果平均值大于1,意味着系统繁忙,任务需要等待,无法及时执行 - 如果 1min 平均值高于 5min 或 15min 平均值,则负载正在增加 - 如果 1min 平均值低于 5min 或 15min 平均值,则负载正在减少 **详解 sar -q** - `runq-sz`: 执行队列的长度 - `plist-sz`:运行中的任务(进程&线程)总数 - `ldavg-1`:最近 1min 系统平均负载 - `ldavg-5`:最近 5min 系统平均负载 - `ldavg-15`:最近 15min 系统平均负载 > 如果它们大于处理器的数量,那么系统及有可能遇到了性能问题 **系统调度观察实验** - 通过 Linux 性能工具观察进程调度 - 单处理器运行过程 - 多处理器运行过程 ```c int main(void) { printf("pid = %d, ppid = %d, pgid = %d\n", getpid(), getpid(), getpgrp()); printf("hello world!\n"); while(1); return 0; } ``` **系统调度核心新能指标** - 吞吐量:单位时间内的工作总量(越大越好) - 处理器资源消耗越多(空闲状态占比越低),吞吐量越大 - 对于进程调度而言,吞吐量指单位时间处理的进程数量 - 延迟:从开始处理任务到结束处理任务所耗费的时间(越短越好) - 对于进程而言,延迟即生命周期,指进程从运行到结束所经历的时间 - 注意:运行和执行不同,运行时间可能很长,但执行时间可能很短 **吞吐量计算一** ![2-multicore_scheduling_3.png](./image/1-process_thread/2-multicore_scheduling_3.png) - 吞吐量 = 1进程/100ms = 1 进程/0.1s = 10进程/秒 - 延迟 = 100ms **吞吐量计算二** ![2-multicore_scheduling_4.png](./image/1-process_thread/2-multicore_scheduling_4.png) - 吞吐量 = 2进程/120ms = 1 进程/0.06s = 16.7进程/秒 - 延迟 = 120ms **吞吐量计算三** ![2-multicore_scheduling_5.png](./image/1-process_thread/2-multicore_scheduling_5.png) - 吞吐量 = 3进程/(40+60*2+20)ms = 1 进程/0.06s = 16.7进程/秒 - 延迟 = 180ms - 假设:每个进程固定执行60ms - 则:进程运行结束时 | 情况 | 吞吐量 | 延迟 | | - | - | - | | 1线程 | 10进程/秒 | 100ms | | 2线程 | 16.7进程/秒 | 120ms | | 3线程 | 16.7进程/秒 | 180ms | | 4线程 | 16.7进程/秒 | 240ms | > 结论: > - 处理器的能力由硬件设计决定,吞吐量存在一个上限 > - 当吞吐量未达上限,进程的延迟取决于进程自身 > - 当吞吐量达到上限,随着进程数量增加,总延迟增加,但平均延迟不变 > **思考:** 如何提高系统吞吐量? > - 提高处理器能力 > - 提高处理器数量 - 多核吞吐量计算 ![2-multicore_scheduling_6.png](./image/1-process_thread/2-multicore_scheduling_6.png) - 吞吐量 = 4进程/120ms = 2 进程/0.06s = 33.3进程/秒 - 延迟 = 120ms > 对于多核处理器计算机来说,只有多个进程并行执行才能提高吞吐量;并且吞吐量也存在一个上限值,当进程数量多于处理器数量时,吞吐量不会再提高 **现实中的系统** - 理想状态:进程正在执行,并且没有就绪状态的进程 - 空闲状态:处理器占用率低,吞吐量低 - 繁忙状态: - 多个进程同时进行,但存在多个就绪状态进程 - 此时,吞吐量很高(可能达到峰值),但总延迟会变长 ## 2-1. Linux 文件基础操作 - Linux 的设计哲学 Linux 中一切皆文件!!! - 什么是文件? - 文件是具有永久存储,按特定字节顺序组成的命名数据集 - 文件可分为:文本文件,二进制文件 - 文本文件:每个字节存放一个 ASCII 码 - 存储量大,速度慢,便于对字符操作 - 二进制文件:数据按照在内存中的存储形式原样存放 - 存储量小,速度快,便于存放中间结果 - Linux 文件编程 在Linux中,除了常规文件,目录,设备,管道等,也属于文件 ![2-file_operate_basic_1.png](./image/2-file_system/2-1-file_operate_basic_1.png) - ASCII C 文件编程 - 标准 C 文件接口建立于 Linux 原生文件接口之上,使用缓冲区机制提高效率 - 缓冲区是一片特殊的内存空间,用来暂存文件中的数据 - 读:可一次性将大量数据读取进入缓冲区(后续再从缓冲区中拿数据) - 写:可先把数据写入缓冲区(缓冲区满之后再把数据一次性写入文件) - 缓冲区的引入是为了避免频繁的磁盘操作,提供文件读写的整体效率 - 深入 ASCII C 文件编程 由于引入了缓冲区,ASCII C 文件编程是一种基于数据流的编程。 ![2-file_operate_basic_2.png](./image/2-file_system/2-1-file_operate_basic_2.png) - ASCII C 文件编程接口 ```C typedef struct _iobuf { char* _ptr; // 文件输入的下一个位置 int _cnt; // 当前缓冲区的相对位置 char* _base; // 文件初始化位置 int _flag; // 文件标志 int _file; // 文件有效性 int _charbuf; // 缓冲区是否可读取 int _bufsiz; // 缓冲区字节数 char* _tmpfname; // 临时文件名 } FILE; ``` ```C FILE *fopen(const char *filename, const char *mode); int fclose(FILE *fp); int fputc(int c, FILE *fp); int fputs(const char *s, FILE *fp); int fgetc(FILE *fp); char *fgets(cahr *buf, int n, FILE *fp); size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file); size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file); ``` - ASCII C 文件打开模式 | 文件使用方式 | 含义 | 如果指定文件不存在 | | -------------- | ------------------------------------------------------ | ------------------ | | `r` (只读) | 为了输入数据,打开一个已经存在的文本文件(从文件获取) | 出错 | | `w` (只写) | 为了输出数据,打开一个文本文件(向文件输出) | 建立一个新的文件 | | `a` (追加) | 向文本文件尾添加数据 | 出错 | | `rb` (只读) | 为了输入数据,打开一个二进制文件 | 出错 | | `wb` (只写) | 为了输出文件,打开一个二进制文件 | 出错 | | `ab` (追加) | 向一个二进制文件尾添加数据 | 出错 | | `r+` (读写) | 为了读和写,打开一个文本文件 | 建立一个新的文件 | | `w+` (读写) | 为了读和写,创建一个新的文本文件 | 建立一个新的文件 | | `a+` (读写) | 打开一个文件,在文件末尾进行读写 | 建立一个新的文件 | | `rb+` (读写) | 为了读和写打开一个二进制文件 | 出错 | | `wb+` (读写) | 为了读和写,新建一个二进制文件 | 建立一个新的文件 | | `ab+` (读写) | 打开一个二进制文件,在文件末尾进行读和写 | 建立一个新的文件 | - 文本文件写示例 > *[2-file_system/1-file_operate_basic/test1.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/1-file_operate_basic/test1.c)* ```C FILE *fp = NULL; char student[50] = {0}; int i = 0; if ((fp = fopen("input.txt", "w")) == NULL) { perror("open file error...\n"); exit(1); } for (int i = 0; i < 3; i++) { printf("input name: "); scanf("%s", student); fputs(student, fp); fputs("\n", fp); } fclose(fp); ``` - ASCII C 文件 “读写指针移动” - `int fseek(FILE *stream, long offset, int whence);` - 移动文件读写指针, `whence` ==> `SEEK_SET`, `SEEK_END`, `SEEK_CUR` - `long ftell(FILE *stream);` - 获取当前读写指针的位置(当对于文件起始位置) - `void rewind(FILE *stream);` - 将读写指针置于文件起始位置,`(void)fseek(stream, 0L, SEEK_SET)` - 二进制文件读写示例 > *[2-file_system/1-file_operate_basic/test2.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/1-file_operate_basic/test2.c)* ```C #define N 3 struct student { long num; char name[10]; int age; }s[N]; for (int i = 0; i < N; i++) { printf("Number: %d\n", i + 1); printf("ID: "); scanf("%ld", &s[i].num); printf("Name: "); scanf("%s", s[i].name); printf("Age: "); scanf("%d", &s[i].age); printf("\n"); } fwrite(s, sizeof(struct student), N, fp); rewind(fp); fread(s, sizeof(struct student), N, fp); for (int i = 0; i < N; i++) { printf("%ld %s %d\n", s[i].num, s[i].name, s[i].age); } fclose(fp); ``` - ASCII C 文件缓冲区类型 - 全缓冲区:默认缓冲区大小为 `BUFSIZ`, 具体大小于系统相关 - 缓冲区满或调用 `fflush()` 后才通过系统调用将数据写入磁盘(设备) - 行缓冲区:默认缓冲区大小为 128 字节,具体大小与系统有关 - 遇见换行符或缓冲区满或调用 `fflush()` 后通过系统调用将数据写入磁盘(设备) - 无缓冲区:不对字符进行缓冲 - 相当于直接使用系统调用 `write()`,数据立即写入磁盘(设备) - 自定义文件缓冲区 ```C #include // 设置大小为 BUFSIZ 的缓冲区 void setbuf(FILE *stream, char *buf); // 设置指定大小的缓冲区 void setbuffer(FILE *stream, char *buf, size_t size); // 设置大小为 128 字节的行缓冲区 void setlinebuf(FILE *stream); // 设置缓冲区 ==> 全缓冲区:_IOFBF 行缓冲区:_IOLBF 无缓冲区:_IONBUF int setvbuf(FILE *stream, char *buf, int mode, size_t size); ``` ```C setbuf(fp, buf); <==> setvbuf(fp, buf, buf ? _IOFBF : _IONFBF, BUFSIZ); setbuffer(fp, buf, sizeof(buf)); <==> setvbuf(fp, buf, buf ? _IOFBF : _IONBF, sizeof(buf)); setlinebuf(fp); <=> setvbuf(fp, NULL, _IOLBF, 0); ``` > 文件缓冲区大小必须大于等于 128 字节!!! - 缓冲区代码示例 > *[2-file_system/1-file_operate_basic/test3.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/1-file_operate_basic/test3.c)* ```C FILE *fp = NULL; char buf[256] = {0}; char *ps = "Hello\nWorld"; if ((fp = fopen("input.txt", "w")) == NULL) { perror("open file error ...\n"); exit(1); } setvbuf(fp, buf, _IOLBF, sizeof(buf)); fwrite(ps, sizeof(*ps), strlen(ps), fp); printf("ps = %s\n", buf); fclose(fp); ``` > *[2-file_system/1-file_operate_basic/test4.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/1-file_operate_basic/test4.c)* ```C struct student { long num; char name[16]; int age; }; FILE *fp = NULL; char buf[BUFSIZ] = {0}; struct student s = {1000, "Fyang", 29}; struct student *ps = NULL; if ( (fp = fopen("input.txt", "w")) == NULL) { printf("open file error ... \n"); exit(1); } printf("BUFSIZ = %d\n", BUFSIZ); setvbuf(fp, buf, _IOFBF, sizeof(buf)); fwrite(&s, sizeof(s), 1, fp); fclose(fp); ps = (void*)buf; printf("num = %ld, name = %s, age = %d\n", ps->num, ps->name, ps->age); ``` ## 2-2. Linux 文件原生操作 - Linux 中一切皆文件,那么 Linux 文件是什么? - 在 Linux 中的文件: - 可以是:传统意义上的有序数据集合,即:文件系统中的物理文件 - 也可以是:设备,管道,内存...(Linux 管理的一切对象) > Linux 利用 VFS 机制,将“设备”以文件系统的形式挂载进内核中,对外提供一致的文件操作接口。 - Linux 中的文件描述符 - 文件描述符是一个整数值,它在内核中被用于标识打开的文件或其他 I/O 资源 - 当打开一个文件时,系统会分配一个文件描述符来唯一标识该文件 - 文件描述符的范围通常是 0 到 1023 - 0, 1, 2 被系统占用,分别代表标准输入,标准输出和标准错误 - 进程中可用的文件描述符范围是 3 到 1023 - Linux 原生文件编程接口 ```c #include #include #include #include int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int creat(const char *pathname, mode_t mode); int close(int fd); ``` ```c int open(const char *pathname, int flags, mode_t mode); ``` | flags | 意义 | | -------- | -------------------- | | O_RDONLY | 只读模式 | | O_WRONLY | 只写模式 | | O_RDWR | 读写模式 | | O_APPEND | 追加模式 | | O_CREAT | 创建并打开一个新文件 | | O_TRUNC | 打开一个文件并截断 | | mode | 值 | 权限 | | ---------------- | ----- | ---------------------------- | | S_IRWXU | 00700 | (当前)可读、可写、可执行 | | S_IRUSR/S_IREAD | 00400 | (当前 可读取 | | S_IWUSR/S_IWRITE | 00200 | (当前)可写入 | | S_IXUSR/S_IEXEC | 00100 | (当前)可执行 | | S_IRWXG | 00070 | (用户组)可读、可写、可执行 | | S_IRWXO | 00007 | (其他)可读、可写、可执行 | | | 00777 | (所有)可读、可写、可执行 | - `ssize_t read(int fd, void* buf, size_t count);` - 从文件中读入数据到 buf 中,返回值为实际读取的数据长度 - `ssize_t write(int fd, const void* buf, size_t count);` - 将数据写入文件中,返回值为实际写入的数据长度 - `off_t lessk(int fd, off_t offset, int whence);` - 移动文件读写指针,`whence` ==> `SEEK_SET`, `SEEK_END`, `SEEK_CUR` - 示例---文件复制 > *[2-file_system/2-native_file_operate/test1.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/2-native_file_operate/test1.c)* ```c if ((src = open(argv[1], O_RDONLY)) == -1) { printf("source: open error ...\n"); exit(-1); } if ((des = open(argv[2], O_CREAT | O_WRONLY, 0777)) == -1) { printf("destination: open error ...\n"); exit(-1); } static int copy(int src, int des) { int ret = 0; char buf[256] = {0}; do { ret = read(src, buf, sizeof(buf)); ret = (ret > 0) ? write(des, buf, ret) : ret; } while (ret == sizeof(buf)); return (ret >= 0); } ``` - 示例---外存数组 - 需求: - 创建一个可以存储“无限”个元素的数组 - 数组大小可动态扩大,即:可动态向数组中追加元素 - 提供统一访问数组元素的方式,即:以 0 下标作为起始地址 - 解决方案 --> 时间换空间 - C 语言中的数组将数据存储在内存中,使用数组前必须定义大小 - 优点:访问速度快 缺点:大小必须固定 - 若要实现可“无限追加”的数组,则需要将数据存储于外存中 - 将数组元素存储于文件中(不必预先定义大小,可实时扩展) - 根据数组元素大小实时定位文件读写位置,并读写固定大小的数据 - 优点:数组大小不受限制 缺点:访问速度较慢 - 外存数组接口设计 ```C typedef void EArray; EArray* EArray_Init(unsigned int n, unsigned int esize); int EArray_Get(EArray* arr, unsigned int index, void *e); int EArray_Set(EArray* arr, unsigned int index, const void *e); int EArray_Append(EArray* arr, const void *e); unsigned int EArray_Length(EArray* arr); void EArray_Release(EArray* arr); ``` - 关键代码设计 ```C int EArray_Get_(EArray *arr, unsigned int index, void *e) { int ret = -1; ExtArray *ea = arr; if (ea && e && (index < ea->length)) { lseek(ea->fd, index * ea->esize, SEEK_SET); ret = read(ea->fd, e, ea->esize); } return ret; } int EArray_Set_(EArray *arr, unsigned int index, const void *e) { int ret = -1; ExtArray *ea = arr; if (ea && e && (index < ea->length)) { lseek(ea->fd, index * ea->esize, SEEK_SET); ret = write(ea->fd, e, ea->esize); } return ret; } ``` - 外存数组应用示例 ```C EArray *arr = EArray_Init_(5, sizeof(int)); printf("length = %d\n", EArray_Length(arr)); for (int i = 0; i < EArray_Length(arr); i++) { int val = i + 1; EArray_Set_(arr, i, &val); } int val = 1000; EArray_Append_(arr, &val); printf("length = %d\n", EArray_Length(arr)); for (int i = 0; i < EArray_Length(arr); i++) { int val = 0; EArray_Get_(arr, i, &val); printf("%d\n", val); } EArray_Release(arr); ``` - 外存数组实现 > *[2-file_system/2-native_file_operate/ext_array](https://gitee.com/fyang0906/linux_go/tree/master/2-file_system/2-native_file_operate/ext_array)* - Linux 一切皆文件? - ASCII C 文件操作: - `stdin` ==> 标准输入流,`stdout` ==> 标准输出流,`stderr` ==> 标准错误输出 - Linux 原生文件操作: - `0` ==> 输入设备,`1` ==> 显示设备,`2` ==> 错误设备 > 可以通过文件操作的方式,完成外部设备的输入输出 - 通过 ASCII C 文件操作 ==> 输入/输出 ```C static void ascii_file_io() { char buf[16] = {0}; int i = 0; char c = 0; do { fread(&c, sizeof(c), 1, stdin); if (c == '\n') break; else buf[i++] = c; } while(i < 16); buf[(i < 16) ? i : 15] = 0; fwrite("output: ", 1, 8, stdout); fwrite(buf, 1, i, stdout); fwrite("\n", 1, 1, stdout); } ``` ```C static void linux_file_io() { char buf[16] = {0}; int i = 0; i = read(0, buf, 16); buf[(i < 16) ? i : 15] = 0; write(1, "output: ", 8); write(1, buf, i); write(1, "\n", 1); } ``` > *[2-file_system/2-native_file_operate/test2.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/2-native_file_operate/test2.c)* ## 2-3. 深入理解文件描述符 > **问题:** 文件描述符只是一个整型值,那么系统是如何利用这个整型值完成文件读写的呢? - 什么是文件系统? - 计算机中用于组织、存储和管理文件的数据结构集合 - 管理磁盘或其它存储介质上的空间(将存储介质分块管理) - 保护文件数据不被破坏,确保数据的一致性和完整性 - 为用户和应用程序提供了访问和操作文件的标准化接口 - 方便的创建、删除、修改、重命名文件 - 实现对文件的读写以及权限控制等功能 - Linux 中文件描述符的本质 - 每个进程都有一个文件描述符表,记录这个进程打开的所有文件 - 文件描述符是文件描述符表中表项的下标(整型值) - 调用 `open()` 打开一个文件时: - 在内核文件打开表中增加一个新表项(记录文件读写指针等信息) - 在进程文件描述符表中增加一个新表项(`open()` 返回表项下标) - 文件描述符表项指向文件打开表项(多对一) ![2-3-file_descriptor_1.png](./image/2-file_system/2-3-file_descriptor_1.png) > `inode`,即:索引节点,存储于硬盘上,用于保存文件的元数据信息,如:文件大小,读/写权限,文件时间戳,等 - 有趣的问题 同一个进程,同时用 `open()` 打开一个文件会发生什么??? - 下面的程序输出什么?为什么? ```C int fd1 = open("test.txt", O_CREAT | O_WRONLY, 0777); int fd2 = open("test.txt", O_RDONLY, 0777); char buf[32] = {0}; int l = 0; int i = 0; if ((fd1 == -1) || (fd2 == -1)) { printf("open file error ...\n"); exit(1); } l = write(fd1, "hello", 5); i += read(fd2, buf, l); buf[i] = 0; printf("buf = %s\n", buf); l = write(fd1, "world", 5); i += read(fd2, buf + i, l); buf[i] = 0; printf("buf = %s\n", buf); close(fd1); close(fd2); ``` > **问题:** > > 1. 进程能否多次打开同一个文件? > 2. 打开后读/写操作能否生效? > 3. 不同文件描述符是否相互影响? > *[2-file_system/3-file_descriptor/test1.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/3-file_descriptor/test1.c)* - 实验结论 - 每次调用 `open()` 函数都返回一个文件描述符(文件描述符相互独立) - `open()` 同一个文件的不同文件描述符对应相同的物理文件 - 不同文件描述符的读写指针不同(均可读写到物理文件中) - 从逻辑上而言,不同文件描述符的读写操作互不影响 > **Note:** 每次 `open()` 都会在文件打开表中加入表项 - 下面的代码输出什么?为什么? ```C int fd = open("test.txt", O_CREAT | O_WRONLY, 0777) int cpid = (fd != -1) ? fork() : -1; if (cpid > 0) { write(fd, "Fyang", 5); while (1) { write(fd, "p", 1); sleep(1); } } else if (cpid == 0) { int i = 0; while (fd > -1) { printf("pos = %ld\n", lseek(fd, 0, SEEK_CUR)); sleep(1); } } ``` > *[2-file_system/3-file_descriptor/test2.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/3-file_descriptor/test2.c)* - `fork()` 与 文件操作 ![2-3-file_descriptor_2.png](./image/2-file_system/2-3-file_descriptor_2.png) > **总结:** 子进程能够看到父进程打开的文件!!! 子进程中的 `fd` 完全相同,指向同一个打开文件表项;因此,父进程的写操作会影响子进程。 - 难以理解的函数... ```C #include int dup(int oldfd); int dup2(int oldfd, int newfd); #define _GUN_SOURCE #include #include int dup3(int oldfd, int newfd, int flags); // flags 重新设置文件打开模式 ``` `dup()` 是系统调用,用于创建一个现有文件描述符的副本,这个新的文件描述符与原文件描述符在许多方面的相同的,它们共享同一个打开文件表项 这意味着他们指向相同的文件,并共享相同的文件偏移量,访问权限等。 - 下面的代码有什么不同? ```C int fd = open("test.txt", O_CREAT | O_WRONLY, 0777); int fd2 = fd; // 数值完全相同 write(fd, "Hello", 5); // √ write(fd2, " ", 1); // √ close(fd2); write(fd, "Fyang", 5); // × 文件已经被关闭 close(fd); close(fd2); ``` ```C int fd = open("test.txt", O_CREAT | O_WRONLY, 0777); int fd2 = open("test.txt", O_CREAT | O_WRONLY, 0777); write(fd, "Fyang", 5); // Fyang write(fd2, " ", 1); // ' 'yang close(fd2); write(fd, "Yang", 4); // ' 'yangYang close(fd); close(fd2); ``` ```C int fd = open("text.txt", O_CREAT | O_WRONLY, 0777); int fd2 = dup(fd); write(fd, "Fyang", 5); write(fd2, " ", 1); // Fyang' ' close(fd2); write(fd, "Yang", 4); // Fyang' 'Yang close(fd); close(fd2); ``` > *[2-file_system/3-file_descriptor/test3.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/3-file_descriptor/test3.c)* - Linux 重定向 对原来系统默认执行方式进行改变,比如:不希望再显示器进行输出而是希望将保存到指定文件,此时可通过 Linux 重定向来完成这项工作 ![2-3-file_descriptor_3.png](./image/2-file_system/2-3-file_descriptor_3.png) - 输出重定向示例 ```C int fd = open("output.txt", O_CREAT | O_TRUNC | O_WRONLY, 0777); int i = 0; int j = 0; close(1); dup2(fd, 1); close(fd); for (i = 1; i <= 9; i++) { for (j = 1; j <= i; j++) { printf("%d * %d = %d ", j, i, j * i); } printf("\n"); } ``` > *[2-file_system/3-file_descriptor/test4.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/3-file_descriptor/test4.c)* ## 2-4. 文件属性编程与权限编程 > **问题:** 如何获取文件的大小,时间戳以及类型等信息? - 再论 `inode` - 文件的物理载体是硬盘,硬盘的最小存储单位是扇区(每个扇区 512 字节) - 512 byte(扇区) * 8 = 4096 byte ==> 一页 - 文件系统以块为单位(每块 8 个扇区)管理文件数据 - 文件元信息(创建者、创建日期、文件大小、等)存储于 `inode` 区域 - `inode` 即:索引节点,文件系统将硬盘分为索引节点区与文件数据区 - 文件 = 文件属性 + 文件内容(通过 `inode` 编号关联) > 索引节点中包含块指针数组,每个数组元素指向一个文件数据块 ![2-4-file_attribute_1.png](./image/2-file_system/2-4-file_attribute_1.png) - 获取文件元信息(即:文件属性) ```c struct stat { dev_t st_dev; /* 如果是设备,返回文件使用的设备号,否则为 0 */ ino_t st_ino; /* 索引节点号 i 节点 */ mode_t st_mode; /* 文件类型与权限 */ nlink_t st_nlink; /* 文件的硬连接数,如果是目录文件,就是一级子目录个数 */ uid_t st_uid; /* 所有者用户识别号 */ gid_t st_gid; /* 组识别号 */ dev_t st_rdev; /* 设备类型 */ off_t st_size; /* 文件大小,字节表示 */ blksize_t st_blksize; /* 系统每次按块 IO 操作时的块大小 */ blkcnt_t st_blocks; /* 块的索引号 */ time_t st_atime; /* 最后访问时间,如 read */ time_t st_mtine; /* 最后修改时间,日历时间 */ time_t st_ctime; /* 创建时间 */ }; ``` ```c #include #include #include // 获取 pathname 指向文件的属性(最终目标文件的属性) int stat(const char *pathname, struct stat *statbuf); // 获取 pathname 指向文件的属性(符号连接文件不解析跳转) int lstat(const char *pathname, struct stat* statbuf); // 获取文件描述符 fd 指向文件的属性 int fstat(int fd, struct stat *statbuf); ``` - 获取文件元信息 ```c S_ISREG(m) // 是否是一个常规文件 S_ISDIR(m) // 是否是一个目录 S_ISCHR(m) // 是否是一个字符设备 S_ISBLK(m) // 是否是一个块设备 S_ISIFO(m) // 是否是输入输出(管道) S_ISLNK(m) // 是否是符号链接 S_ISSOCK(m) // 是否是网络套接字 ``` - 基础示例 ```c struct stat file_stat = {0}; int ret = -1; ret = stat(argv[1], &file_stat); if (ret == -1) { perror("stat error ...\n"); exit(ret); } printf("file size: %ld bytes.\n" "file mode: 0%o\n" "regual file: %d\n" "block size: %ld\n" "inode num: %ld\n" "create time: %s\n", file_stat.st_size, file_stat.st_mode, S_ISREG(file_stat.st_mode), file_stat.st_blksize, file_stat.st_ino, asctime(localtime(&file_stat.st_ctime))); ``` > *[2-file_system/4-file_attribute/test1.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/4-file_attribute/test1.c)* - 文件权限基本概念 - `struct stat` 结构体中的 `st_mode` 字段记录了文件的访问权限位 - 文件的权限可以分为普通权限和特殊权限 - 普通权限:读,写,执行 - 特殊权限:`Set-User-ID`, `Set-Group-ID` 以及 `Sticky` ![2-4-file_attribute_2.png](./image/2-file_system/2-4-file_attribute_2.png) - `st_mode` 普通权限 | ... | ... | ... | | ------- | --- | ---------------------------------------------- | | S_IRWXU | 700 | owner has read, write and execute permission | | S_IRUSR | 400 | owner has read permission | | S_IWUSR | 200 | owner has write permission | | S_IXUSR | 100 | owner execute permission | | ... | ... | ... | | S_IRWXG | 70 | group has read, write and execute permission | | S_IRGRP | 40 | group has read permission | | S_IWGRP | 20 | group has write permission | | S_IXGRP | 10 | group has execute permission | | ... | ... | ... | | S_IRWXO | 7 | others have read, write and execute permission | | S_IROTH | 4 | others have read permission | | S_IROTH | 2 | others have write permission | | S_IXOTH | 1 | others have execute permission | - 文件权限基本概念 | 文件关联ID | | -------------------------------- | | 文件所有者ID | | 文件所有者组ID(仅限可执行文件) | | 文件设置组ID(仅限可执行文件) | | | 进程关联ID | | ------------------------------ | ---------------- | | 实际用户ID

实际组ID | 标识进程自身 | | 有效用户ID

有效组ID | 文件访问权限检查 | > 一般情况下,用户登录之后运行程序所得到的进程,实际用户ID 和 有效用户 ID 相同! - 深入用户ID - 实际用户ID(RUID):即 Linux 登录ID - 用户登录后,运行程序所得的进程的用户ID - 有效用户ID(EUID):决定用户对系统资源的访问权限 - 用于判断进程是否拥有某个文件的操作权限(读,写,执行) - 设置用户ID:文件权限中的特殊标志位(二进制 bit 位) - 只能用于可执行文件,其作用是标志可修改有效用户ID - `st_mode` 特殊权限 | 宏 | 值 | 意义 | | ------- | ----- | -------------------------------------------------------------------------------------------------- | | S_ISUID | 04000 | Set-User-ID: 程序特殊权限位,进程直接获取了文件所有者的权限、以文件所有者的身份操作该文件 | | S_ISGID | 02000 | Set-Group-ID: 程序特殊权限位,进程直接获取了文件所属组成员的权限,以文件所属组成员的身份操作该文件 | | S_ISVTX | 01000 | Sticky:目录特殊权限位,只有文件所有者或者超级用户可以删除或重命名指定目录下的文件 | - 文件权限编程 ```C #include #include // 改变 path 指向文件的权限位 int chmod(const char *path, mode_t mode); // 改变 fd 指向文件的权限位 int fchmod(int fd, mode_t mode); // 检查 pathname 指向的文件是否具有 mode 权限 int access(const char *pathname, int mode); // 设置新创建的文件和目录的掩码值 mode_t umask(mode_t mask); ``` - 文件权限编程示例一 ```C int check(const char *filename) { // 检查文件是否存在 if (access(filename, F_OK) == -1) { printf("File does not exist.\n"); return 1; } // 检查文件的读权限 if ((access(filename, R_OK | W_OK | X_OK) == 0) && (errno == 0)) { printf("File has all the required permissions.\n"); } return 0; } ``` - 文件权限编程示例二 ```C int i = 0; unsigned int mode = file_stat.st_mode; printf("Common Attribute: "); for (i = 0; i < 3; i++) { char pri[] = "---"; int cm = (mode >> (6 - i * 3)) & 0x7; if (cm & 0x1) pri[2] = 'x'; if (cm & 0x2) pri[1] = 'w'; if (cm & 0x4) pri[0] = 'r'; printf("%s", pri); } printf("\n"); printf("Special Attribute: "); { char pri[] = "---"; int cm = (mode >> 9) & 0x7; if (cm & 0x1) pri[2] = 'x'; if (cm & 0x2) pri[1] = 'g'; if (cm & 0x4) pri[0] = 'u'; printf("%s", pri); } printf("\n"); ``` > *[2-file_system/4-file_attribute/test2.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/4-file_attribute/test2.c)* - 文件权限编程示例三 ```C int mode = strtol(argv[2], NULL, 8); int fd = 0; int pre = umask(022); printf("pre = %o\n", pre); printf("mdoe = %o\n", mode); unlink(argv[1]); fd = open(argv[1], O_CREAT | O_TRUNC | O_WRONLY, 0777); printf("fd = %d\n", fd); close(fd); privilege(argv[1]); chmod(argv[1], mode); privilege(argv[1]); umask(pre); ``` > *[2-file_system/4-file_attribute/test3.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/4-file_attribute/test3.c)* ## 2-5. 目录本质剖析与编程 > *如果直接用 `open()` 打开目录(文件夹)进行读写,会发生什么?* - 目录的本质 - 目录本质上是一个特殊的文件(Linux 中一切皆文件!!!) - 文件: - 文件的 `inode` 存储了元数据及指向数据块的指针 - 数据块存储了文件的数据内容(裸数据,无特定数据格式) - 目录: - 目录的 `inode` 存储了元数据及指向数据块的指针 - 不同的是,数据块存储的是一系列文件项,文件项包含一个文件名和一个 `inode` 号 - 目录和文件的区别 - 目录:目录的 `inode` 不是直接指向数据内容,而是指向一种特殊的数据结构,通常称之为目录项(Directory Entries)。每一个目录项包括两部分,一是文件名,指向该文件(子目录)的 `inode` 指针。 - 文件:文件的 `inode` 存储了指向文件内容所在的数据块的指针,文件的内容就保存在这些数据块中;文件的 `inode` 还保存了文件的各种属性,如文件大小、创建时间、所有者信息等。 > 目录实际上是一个特殊的文件,它的内容是一种映射关系,即文件名到 `inode` 的映射。因此,目录可以包含其它文件或目录,这就构成了我们常见的文件系统的树状结构。 > > `read()/write()` 系统调用直接读写 `inode` 所指数据块的内容,而无法读写目录项所包含的内容!!! - Linux 目录操作函数 ```C #include #include struct dirent { ino_t d_ino; /* Inode number */ off_t d_off; /* Not an offset; see below */ unsigned short d_reclen; /* Length of this record */ unsigned char d_type; /* Type of file */ char d_name[256]; /* Null-terminated filename */ }; DIR *opendir(const char *name); DIR *fdopendir(int fd); struct dirent* readdir(DIR *dirp); int closedir(DIR *dirp); ``` ```C int fd = open("testdir", O_RDONLY); struct stat file_stat = {0}; fstat(fd, &file_stat); printf("dir = %d\n", S_ISDIR(file_stat.st_mode)); if ((fd != -1) && (S_ISDIR(file_stat.st_mode))) { DIR *dir = fdopendir(fd); struct dirent* ent = NULL; while (dir && (ent = readdir(dir))) { printf("ent->d_name = %s\n", ent->d_name); printf("ent->d_inode = %ld\n", ent->d_ino); printf("\n"); } closedir(dir); } else { printf("can NOT open a directory ...\n"); } close(fd); ``` > *[2-file_system/5-dir/test2.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/5-dir/test2.c)* - Linux 目录操作函数 ```C #include #include #include #include int mkdir(const char *pathname, mode_t mode); int mkdirat(int dirfd, const char *pathname, mode_t mode); int rmdir(const char *pathname); DIR *telldir(const char *name); // 获取当前目录读写指针位置 void rewinddir(DIR *dirp); // 将目录读写指针移回初始位置 void seekdir(DIR *dirp, long loc); // 将目录读写指针移动到指定位置 ``` - 下面的程序实现了什么功能? ```C struct dirent *ent = NULL; long pos = telldir(dir); while (dir && (ent = readdir(dir))) { long current = telldir(dir); if (strcmp(ent->d_name, "main.c") == 0) { break; } else { pos = current; } } if (ent) { printf("pos = %ld\n", pos); seekdir(dir, pos); ent = readdir(dir); printf("name = %s\n", ent->d_name); } else { printf("target is NOT existed...\n"); } ``` > *[2-file_system/5-dir/test3.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/5-dir/test3.c)* - 下面的代码有没有问题? ```C const char *name = "testdir"; DIR *dir = opendir(fd); if (dir) { struct dirent *ent = NULL; while ((ent = readdir(dir)) != NULL) { printf("name = %s\n", ent->d_name); rmdir(ent->d_name); } closedir(dir); rmdir("testdir"); } else { printf("can NOT open a directory ...\n"); } ``` > *[2-file_system/5-dir/test4.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/5-dir/test4.c)* > **NOTE:** > > 1. 启动目录:应用程序当前所在的目录 ==> `/usr/home/test/a.out` > 2. 当前目录:当应用程序加载执行成为进程之后为当前目录 > 3. 路径:绝对路径/相对路径 - Linux 目录操作函数 ```C #include // 获取当前绝对路径,并存储于 buf 中 // 如果路径长度大于 size,返回 NULL char *getcwd(char *buf, size_t size); // 获取当前绝对路径,并存储于 buf 中 // 如果路径长度大于 PATH_MAX,返回 NULL char *getwd(char *buf); // 返回当前绝对路径,返回值需要被释放 char *get_current_dir_name(void); // 修改当前绝对路径为 path 或 fd 所指向的目录 int chdir(const char *path); int fchdir(int fd); ``` - 目录操作示例 ```C const char *name = "input.txt"; int r = access(name, R_OK); char *path = get_current_dir_name(); char buf[PATH_MAX] = {0}; printf("current path: %s\n", path); printf("access: r = %d\n", r); free(path); r = chdir("/home"); printf("chdir: r = %d\n", r); getcwd(buf, sizeof(buf)); r = access(name, R_OK); printf("current path: %s\n", buf); printf("access: r = %d\n", r); ``` > *[2-file_system/5-dir/test5.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/5-dir/test5.c)* ## 2-6. 目录与文件编程练习 > **问题:** 如何编程实现目录的拷贝?与目录删除的方法相同吗? - 综合小练习 - 需求:编写程序拷贝指定目录 - 通过命令行参数指定源目录与目标目录 - 源目录中的文件与子目录需要完整拷贝 - 出现目标目录中的同名文件冲突时提示是否覆盖 - 目标目录中的其它文件不受拷贝操作的影响 - 解决方案 目录由包含的文件和子目录构成,所以目录的拷贝就是所包含文件和所包含子目录的拷贝!!! ``` ------ copy_file(src + file, des) // file 为 src 中的文件 | copy(src, des) --| | ------ copy_dir(src + dir, des + dir) // dir 为 src 中的子目录 ``` - 需要关注的重点 > 如何保证 “完全拷贝”? Linux 中的目录与文件除了关键数据信息之外,还有元数据信息。因此,“完整拷贝” 指关键数据与元数据都需要进行拷贝。 > 元数据信息中的权限信息对用户是至关重要的,因此重点考虑如何拷贝权限信息!!! ```C int pre = umask(0000); int mode = get_mode(src); int src_fd = (mode != -1) ? open(src, O_RDONLY) : -1; int des_fd = -1; remove(des); if ( src_fd != -1) { des_fd = open(des, O_CREAT | O_WRONLY, mode); } ``` ```C int pre = umask(0000); int mode = get_mode(src); if (mkdir(des, mode) == 0) { ret = do_copy_dir(src, des); } umask(pre); ``` - 拷贝时需要注意的关键问题 | 源 src | 目标 des | 操作 | | ------ | -------- | ---------------------------------------- | | 文件 | 文件 | 直接进行文件拷贝 src -> des | | 文件 | 目录 | 将文件拷贝到指定目录 src -> des/src | | 目录 | 文件 | 拷贝失败 | | 目录 | 目录 | 将 src 中包含的文件和子目录拷贝到 des 中 | > **注意:** 从源目录向目标目录进行拷贝,可分解为,源目录中的文件与源目录中的子目录向目标目录进行拷贝;因此,整个过程是一个递归过程,在代码实现上需要进行递归操作。 - 关键实现逻辑 ```C static int copy(const char *src, const char *des) { int src_type = get_type(src); int ret = -1; if (src_type == 0) { ret = copy_file(src, des); } if (src_type == 1) { ret = copy_dir(src, des); } return ret; } ``` ```C static int do_copy_dir(const char *src, const char *des) { DIR *dir = opendir(src); struct dirent *ent = NULL; int ret = dir ? 0 : -1; while (dir && (ent = readdir(dir))) { if ( (strcmp(ent->d_name, ".") != 0) && (strcmp(ent->d_name, "..") != 0) ) { char nsrc[PATH_MAX] = {0}; char ndes[PATH_MAX] = {0}; sprintf(nsrc, "%s/%s", src, ent->d_name); sprintf(ndes, "%s/%s", des, ent->d_name); ret += copy(nsrc, ndes); } } closedir(dir); return ret; } ``` - 目录拷贝 > *[2-file_system/6-dir_file/test.c](https://gitee.com/fyang0906/linux_go/blob/master/2-file_system/6-dir_file/test.c)* > **思考:** 利用文件能够实现外存数组,那么是否能够实现外存线性表呢? - 线性表 - 线性表可以采用顺序存储结构实现: - 零个或多个数据元素组成的集合 - 数据元素在位置上是有序排列的 - 数据元素的个数是有限的 - 数据元素的类型必须相同 - 线性表(List)的性质 - a ``0 `` 为线性表的第一个元素,只有一个后继 - a ``n-1 `` 为线性表的最后一个元素,只有一个前驱 - 除 a ``0 `` 和 a ``n-1 `` 外的其他元素 a ``i `` 既有前驱,又有后继 - 直接支持逐项访问和顺序存取 - 线性表(List)的操作 - 将元素插入线性表 - 将元素从线性表中删除 - 获取目标位置处元素的值 - 设置目标位置处元素的值 - 获取线性表的长度 - 清空线性表 ```C EList* EList_Init(type); type EList_Get(type, list, index); int EList_Set(type, list, index, v); int EList_Insert(type, list, index, v); int EList_Append(type, list, v); type Elist_Remove(type, list, index); int Elist_Length(list); int Elist_Clear(list); void EList_Release(list); ``` - 文件截断 ```C #include #include // 将文件截断为指定大小 int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length); ``` - 外存顺序表的关键实现:插入元素 ![2-6-dir_file_1.png](./image/2-file_system/2-6-dir_file_1.png) ```C int i = ea->length - 1; ret = 1; while ( (i >= (int)index) && ret ) { lseek(ea->fd, i * ea->esize, SEEK_SET); ret = (read(ea->fd, buf, ea->esize) != -1) && (write(ea->fd, buf, ea->esize) != -1); i--; } lseek(ea->fd, index * ea->esize, SEEK_SET); ret = ret ? write(ea->fd, e, ea->esize) : -1; if (ret != -1) { ea->length += 1; } ``` - 外存顺序表的关键实现:删除元素 ![2-6-dir_file_2.png](./image/2-file_system/2-6-dir_file_2.png) ```C int i = index + i; ret = 1; while ( (i >= (int)index) && ret) { lseek(ea->fd, i * ea->esize, SEEK_SET); ret = (read(ea->fd, buf, ea->esize) != -1) && (write(ea->fd, buf, ea->esize) != -1); i--; } lseek(ea->fd, index * ea->esize, SEEK_SET); ret = ret ? write(ea->fd, e, ea->esize) : -1; if ( ret != -1 ) { ea->length += 1; } ``` - 外存顺序表设计与实现 > *[2-file_system/6-dir_file/ext_list](https://gitee.com/fyang0906/linux_go/tree/master/2-file_system/6-dir_file/ext_list)*