unix环境高级编程=UNIX基础知识 《APUE》第1章这23个标题逐条用大白话讲清楚并且告诉你每一条用 Swoole6合适、还是用原生 PHP/posix 扩展更合适再给可直接跑的代码。 先把结论说在前面很重要 ▎ Swoole6不是用来替代操作系统概念的它是 PHP 层对这些系统能力的封装。 ▎-进程/信号/多进程模型 → 用 Swoole6最爽Swoole\Process。 ▎-真·线程 →Swoole6的新特性 Swoole\Thread需要 ZTS 版 PHP这是第16条的最佳载体。 ▎-文件 I/O/用户ID/时间/errno 这些底层系统调用 →其实用 PHP 内置函数posix 扩展更直接Swoole ▎ 只在协程里不阻塞时才有额外价值。 环境准备前提 php-v # 建议 PHP8.2且为 ZTS 版本线程才可用 php--ri swoole # 确认 Swoole6.x 已安装 php-m|grep-i posix # 第4/8/19等条需要 posix 扩展---1.操作系统内核kernel 大白话内核就是机器的大管家。CPU、内存、磁盘、网卡这些硬件你不能直接碰你想用得喊一声让管家去办。你写的所有程序 都跑在管家管的地盘里。 用谁纯概念没专属 API。能做的就是问问管家自己是谁。?php// 看内核信息这背后就是 uname(2) 系统调用print_r(posix_uname());// 输出 sysnameLinux release6.x ... 这就是内核echo PHP_OS. / .php_uname().PHP_EOL;---2.系统调用system call 大白话系统调用就是你正式喊管家办事的那句暗号。比如 open、read、write 都是暗号。你的程序在用户态喊暗号后会切到内核态由管家执行办完再切回来。切换是有成本的。 用谁PHP/Swoole 的每个 I/O 函数底层都是系统调用。验证方式是用 strace 看暗号。?php// 这一行 fwrite 底层就会触发一次 write(2) 系统调用fwrite(STDOUT,hello syscall\n);# 跑起来看暗号Linux能看到write(1,hello syscall\n,14)strace-e tracewrite php demo.php---3.shell 大白话shell 是个翻译官传令兵。你打字 ls-l它翻译成系统调用去执行程序再把结果给你。它本质也是一个普通程序。 用谁执行 shell 命令——协程里强烈推荐Swoole 的 Coroutine\System::exec不阻塞其他协程普通脚本用 shell_exec 也行。?php use Swoole\Coroutine;use Swoole\Coroutine\System;Coroutine\run(function(){// 协程版执行 shell等命令时不会卡住整个进程$retSystem::exec(ls -l /tmp);echo退出码: {$ret[code]}\n;echo $ret[output];});---4.登录名//etc/passwd 大白话/etc/passwd 是一张用户花名册每行记录一个用户登录名、UID、家目录、默认 shell。你的登录名就是花名册里的一行。 用谁用 posix 扩展最直接Swoole 不掺和。?php echo登录名: .posix_getlogin().PHP_EOL;$infoposix_getpwuid(posix_getuid());// 查花名册print_r($info);// name, dir(家目录), shell, gid ...---5.文件与目录 大白话UNIX 里一切皆文件。普通文件、目录、设备、甚至网络连接都用同一套接口去读写。目录其实就是装着文件名的特殊文件。 用谁纯文件操作用 PHP 内置即可协程里用 Coroutine\System::readFile/writeFile 不阻塞。?php use Swoole\Coroutine;use Swoole\Coroutine\System;Coroutine\run(function(){System::writeFile(/tmp/a.txt,你好\n);echoSystem::readFile(/tmp/a.txt);echois_dir(/tmp)?/tmp 是目录\n:;});---6.文件系统树/根目录/大白话整个文件系统是一棵倒挂的树最顶上的根叫/。所有东西都从/往下挂没有 Windows 那种 C盘 D盘盘是挂到树上某个目录的。 用谁PHP 内置 scandir 即可。?phpforeach(scandir(/)as $name){// 列出根目录这一层if($name.||$name..)continue;echo/$name\n;}---7.工作目录cwd 大白话进程当前站在哪个目录里。你写相对路径 a.txt找的就是工作目录下的 a.txt。cd 就是换工作目录。 用谁PHP 内置 getcwd/chdir。?php echo现在在: .getcwd().PHP_EOL;chdir(/tmp);// 走到 /tmpecho现在在: .getcwd().PHP_EOL;// 之后相对路径都基于这里---8.起始目录home 大白话你登录后默认待的家比如/home/张三。环境变量 HOME 记着它。 用谁getenv 或 posix 查花名册。?php echoHOME环境变量: .getenv(HOME).PHP_EOL;$pwposix_getpwuid(posix_getuid());echo花名册里的家: .$pw[dir].PHP_EOL;// 更可靠---9.文件描述符file descriptor 大白话你打开一个文件内核给你一个小号码牌0、1、2、3…以后你拿号码牌找内核操作这个文件而不是报全名。号码牌就 是文件描述符。 用谁PHP 的资源句柄背后就是 fd。想看真实数字用 socket/stream 配合。?php $fpfopen(/tmp/a.txt,w);// 在 Linux 上能通过 /proc 反查这个流的 fd 号$id(int)filter_var((string)$fp,FILTER_SANITIZE_NUMBER_INT);echo拿到一个文件句柄(底层就是fd): $fp\n;fclose($fp);---10.标准输入/输出/错误STDIN/STDOUT/STDERR 大白话每个进程一出生就自带3个号码牌0输入键盘、1正常输出屏幕、2错误输出屏幕。把正常和错误分开是为了能单独把错误重定向走。 用谁PHP 内置常量 STDIN/STDOUT/STDERR。?phpfwrite(STDOUT,这是正常输出(fd1)\n);fwrite(STDERR,这是错误输出(fd2)\n);// 运行: php demo.php 1out.log 2err.log 就能分开收---11.不带缓冲的 I/Oopen read write lseek close 大白话这五个是最原始的读写五件套每调一次就直接喊一次内核不在中间攒数据无缓冲。lseek 是把读写指针挪到文件某个位置。 用谁PHP 内置 fopen/fread/fwrite/fseek/fclose 一一对应要真正绕过用户态缓冲open 时用 b 模式并配合stream_set_write_buffer($fp,0)。?php $fpfopen(/tmp/raw.txt,cb);// openstream_set_write_buffer($fp,0);// 关掉用户态缓冲更接近无缓冲fwrite($fp,ABCDEFG);// writefseek($fp,2,SEEK_SET);// lseek 到第2字节echofread($fp,3).PHP_EOL;// read - CDEfclose($fp);// close---12.标准 I/Oprintf getc putc fgets 大白话第11条太原始标准 I/O 库在上面包了一层带缓冲的便利函数帮你攒够一批再一次性写省得频繁喊内核。printf 格式化输出fgets 按行读。 用谁PHP 内置 printf/fgetc/fputc/fgets。?phpprintf(姓名:%s 年龄:%d\n,张三,18);// printf$fpfopen(/tmp/raw.txt,r);echofgetc($fp).PHP_EOL;// getc: 读一个字符echofgets($fp).PHP_EOL;// fgets: 读一行fclose($fp);---13.程序与进程program/process 大白话程序是躺在硬盘上的一个文件死的、菜谱进程是这个程序被装进内存跑起来后的样子活的、正在炒菜。同一个程序 可以同时跑出好几个进程。 用谁概念。看自己这个进程。?php echo我这个进程的PID .getmypid().PHP_EOL;echo我是由这个程序文件跑起来的: .__FILE__.PHP_EOL;---14.进程 IDPIDgetpid 大白话每个进程都有一个唯一工号PID。内核靠工号管理它你用 kill PID 也是靠工号找它。 用谁PHP 内置getmypid()或posix_getpid()。?php echoPID: .posix_getpid().PHP_EOL;echo父进程PID(谁生的我): .posix_getppid().PHP_EOL;---15.进程控制fork exec waitpid ⭐ Swoole6主场 大白话-fork进程分身一个变两个俩几乎一模一样父、子。-exec让某个进程改头换面把自己换成另一个程序来跑。-waitpid父进程等娃干完活并收尸避免僵尸进程。 用谁Swoole6的 Swoole\Process 是最佳方式比手撸 pcntl_fork 干净安全得多。?php use Swoole\Process;// fork: 创建子进程$childnewProcess(function(Process $worker){echo我是子进程, PID.$worker-pid., 父.posix_getppid().\n;sleep(1);// exec: 把自己变成 /bin/echo 这个程序$worker-exec(/bin/echo,[exec后我就是echo了]);// exec 成功后下面的代码不会执行});$pid$child-start();// 启动子进程echo我是父进程, 生了娃 PID$pid\n;$statusProcess::wait(true);// waitpid: 阻塞等子进程结束并收尸echo娃结束了: ;print_r($status);// [pid, code退出码, signal]---16.线程与线程 ID ⭐ Swoole6新特性 大白话进程之间内存是各过各的隔离线程是同一个进程里的多个执行流共享同一份内存所以通信快但要小心抢同一块数据 。每个线程也有自己的 ID。 用谁Swoole6原生 Swoole\Thread这是6.0的招牌功能需要 ZTS 版 PHP。轻量并发也可用协程Coroutine::getCid()当用户态线程。?php// 需要 ZTS 版 PHP: php -i | grep Thread Safety 应为 enableduse Swoole\Thread;if(!Thread::isThreadContext()){// 主线程开 3 个子线程$threads[];for($i1;$i3;$i){$threads[]newThread(__FILE__,$i);// 把自己当线程脚本再跑}foreach($threads as $t)$t-join();// 等所有线程结束echo主线程结束\n;}else{// 子线程上下文[$i]Thread::getArguments();echo线程#$i 运行中, 线程ID.Thread::getId().\n;}协程版不需要 ZTS最常用?php use Swoole\Coroutine;Coroutine\run(function(){for($i1;$i3;$i){Coroutine::create(function()use($i){echo协程#$i, CID.Coroutine::getCid().\n;// 协程ID≈线程ID});}});---17.出错处理errno、strerror、perror 大白话系统调用出错时不会直接说人话而是设一个错误号码 errno比如2表示文件不存在。strerror 把号码翻成人话perror 顺带打印出来。 用谁Swoole 用swoole_errno()/swoole_strerror()通用系统错误用posix_errno()/posix_strerror()。?php// posix 版fopen(/不存在/x,r);$errposix_get_last_error();// 拿 errnoechoerrno$err, 含义: .posix_strerror($err).PHP_EOL;// swoole 版IO/网络出错时echoswoole最后错误: .swoole_strerror(swoole_last_error()).PHP_EOL;---18.errno.h大白话这是 C 语言里给所有错误号起名字的字典比如 ENOENT2、EACCES13。让你写 ENOENT 而不是记死数字2。 用谁PHP 里这些常量散落在各扩展。socket 类用 SOCKET_E*常量通用的可查表。?php// PHP 里对应的errno字典常量socket 扩展echo SOCKET_ECONNREFUSED. .socket_strerror(SOCKET_ECONNREFUSED).PHP_EOL;echo SOCKET_ENOENT. .socket_strerror(SOCKET_ENOENT).PHP_EOL;---19.用户 ID/组 ID/附加组 ID 大白话系统靠数字身份证认人UID你是谁、GID你的主组、附加组你还兼属哪些组。文件权限就是靠这几个数字判断你 能不能动它。 用谁查身份用 posix进程降权切换身份用 Swoole 的 Process::setUser守护进程常用。?php echoUID.posix_getuid(). GID.posix_getgid().PHP_EOL;echo附加组: .implode(,,posix_getgroups()).PHP_EOL;// Swoole 里让 worker 进程降权运行需 root 启动// $process-setUser(www-data); $process-setGroup(www-data);---20.信号signalsignal kill ⭐ Swoole6主场 大白话信号是发给进程的一封软件电报比如 SIGINT你按 CtrlC、SIGTERM请你正常退出、SIGKILL强制干掉。kill 是发电报signal 是我收到电报后怎么办。 用谁Swoole Process::signal/Process::kill 最佳协程友好不丢信号。?php use Swoole\Process;// 注册收到 SIGTERM 时优雅退出Process::signal(SIGTERM,function($signo){echo收到电报 SIGTERM($signo)准备优雅退出\n;Process::kill(posix_getpid(),SIGKILL);});echo我的PID.posix_getpid().现在试试: kill -TERM .posix_getpid().\n;// 让进程活着等信号\Swoole\Event::wait();---21.时间值日历时间、进程时间时钟/用户CPU/系统CPU 大白话两类时间——-日历时间墙上挂钟的时间从1970年起的秒数。-进程时间你这进程实际干活花的 CPU又分用户CPU跑你自己代码和系统CPU喊内核办事。-还有墙钟耗时real从开始到结束实际过了多久。 用谁PHP 内置 microtimegetrusage。?php $startmicrotime(true);$r0getrusage();for($i0;$i2_000_000;$i){$xsqrt($i);}// 干点活$realmicrotime(true)-$start;$r1getrusage();$user($r1[ru_utime.tv_sec]-$r0[ru_utime.tv_sec])($r1[ru_utime.tv_usec]-$r0[ru_utime.tv_usec])/1e6;$sys($r1[ru_stime.tv_sec]-$r0[ru_stime.tv_sec])($r1[ru_stime.tv_usec]-$r0[ru_stime.tv_usec])/1e6;printf(墙钟real%.3fs 用户CPU%.3fs 系统CPU%.3fs\n,$real,$user,$sys);---22.time/times 大白话time 拿现在几点日历时间times 拿这进程到现在烧了多少 CPU。就是第21条那两类时间的取值函数。 用谁PHP 内置time()↔timegetrusage()↔times。?php echo日历时间(time): .time(). .date(Y-m-d H:i:s).PHP_EOL;$ugetrusage();// 对应 times()printf(进程已用CPU: 用户%.3fs 系统%.3fs\n,$u[ru_utime.tv_sec]$u[ru_utime.tv_usec]/1e6,$u[ru_stime.tv_sec]$u[ru_stime.tv_usec]/1e6);---23.系统调用与库函数的区别 大白话-系统调用直接喊内核管家办事write、open要切到内核态慢但是唯一入口。-库函数在用户态先帮你打理好攒缓冲、格式化攒够了才喊一次内核printf 内部最终调 write。-一句话库函数是管家秘书帮你少跑几趟、跑得漂亮真要办事还得秘书去喊管家。 用谁用代码对比调用次数最直观——库函数缓冲版只触发1次系统调用无缓冲版触发 N 次。?php// 库函数(带缓冲): 攒一批底层只 write 一两次$fpfopen(/tmp/buffered.txt,w);// 默认带缓冲for($i0;$i1000;$i)fwrite($fp,x);fclose($fp);// 这时才真正刷到内核// 近似系统调用直连: 关缓冲每次都喊内核$fpfopen(/tmp/unbuffered.txt,w);stream_set_write_buffer($fp,0);for($i0;$i1000;$i)fwrite($fp,x);// 触发约1000次 write(2)fclose($fp);// 用 strace -c -e write 跑这两段对比 write 次数即可看出差别strace-c-e tracewrite php demo23.php # 看 write 调用次数差异---总结哪条该用什么一张表 ┌───────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────┐ │ 标题 │ 最佳工具 │ 说明 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │1,2,5,6,7,8文件系统/cwd/home │ 原生 PHP │ Swoole 无增益 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │3shell │ SwooleSystem::exec(协程)/shell_exec │ 协程里选 Swoole │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │4,17,18,19,21,22用户/errno/时间 │ posix内置 │ 系统信息查询Swoole 不掺和 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │9,10,11,12文件描述符/标准IO │ 原生 PHP协程里用 Coroutine\System │ 不阻塞才上 Swoole │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │15fork/exec/wait │ ⭐ Swoole Process │ Swoole 主场 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │16线程 │ ⭐ Swoole6Thread(ZTS)/协程 │6.0招牌功能 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │20信号 │ ⭐ Swoole Process::signal │ 协程友好 │ ├───────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────┤ │13,14,23进程概念/PID/syscall对比 │ 原生strace │ 概念演示 │ └───────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────┘ 一句话选型凡是多进程、信号、线程、协程并发这类运行时模型——用Swoole6凡是查系统信息、读写单个文件、算时间、看errno——用原生PHPposix 更直接。