Linux进程与通信基础:进程、信号、管道与IO重定向
Linux进程与通信基础进程、信号、管道与IO重定向单个程序好写但让多个程序协同工作——一个的输出喂给另一个的输入、在恰当的时候让它停下来、把日志自动写到文件里——这些就需要进程间通信的本事了。本文带你搞懂Linux进程通信的核心机制。一、进程基础概念1.1 程序 vs 进程程序磁盘上的可执行文件静态的只是一堆指令和数据进程程序被加载到内存后运行起来的实例动态的有生命周期打个比方程序是菜谱进程是按菜谱做菜的过程。1.2 进程的组成部分每个进程在内核中由task_struct结构体管理包含以下关键信息属性说明PID进程ID唯一标识符PPID父进程IDUID/GID进程的身份信息状态运行®/睡眠(S)/停止(T)/僵尸(Z)等优先级nice值-20~19越小优先级越高虚拟内存空间代码段、数据段、堆、栈文件描述符表打开的文件、设备、套接字1.3 进程的创建fork与execLinux创建新进程的经典两步走1. fork() → 父进程创建一个自己的副本子进程 2. exec() → 子进程用新程序替换自己的代码段# 用strace观察一个简单命令的fork/exec过程strace-etracefork,execvels在Shell中执行任何命令Shell都会先fork出子进程再在子进程中exec加载目标程序。1.4 进程状态详解fork │ ▼ ┌──────────────────────────┐ │ 运行 (R/Running) │ ←── 获得CPU时间片 │ │ │ 等待事件 被信号停止 │ │ │ │ │ ▼ ▼ │ │ ┌──────┐ ┌─────────┐ │ │ │睡眠(S)│ │停止(T) │ │ │ └──┬───┘ └────┬────┘ │ │ │ │ │ │ │ 事件完成 │ SIGCONT │ │ │ │ │ └──────────────┘ │ │ │ │ exit() │ │ │ │ │ ▼ │ │ ┌────────────┐ │ │ │ 僵尸(Z) │ │ │ │ 等父进程回收 │ │ │ └────────────┘ │ └───────────────────────────┘状态字母说明运行R正在CPU上执行或在就绪队列中等待睡眠S等待某个事件如IO完成可被信号唤醒不可中断睡眠D等待IO不响应信号kill -9也杀不掉停止T被信号暂停如CtrlZ可恢复僵尸Z已终止但父进程尚未回收其状态信息1.5 僵尸进程问题子进程退出后父进程需要调用wait()来回收子进程的退出状态。如果父进程不回收子进程就变成僵尸进程——占用PID资源但不占内存。# 查找僵尸进程psaux|grepZ# 找到僵尸进程的父进程ps-oppid-p僵尸进程PID# 杀死父进程让init接管回收sudokill父进程PID1.6 守护进程守护进程Daemon是在后台运行的长期服务进程不依赖终端。# 查看系统中的守护进程psaux|grepd$# 常见守护进程# sshd - SSH服务# cron - 定时任务# systemd - 系统初始化# networkd - 网络管理创建守护进程的典型步骤#includeunistd.h#includesys/stat.hintmain(){// 1. fork出子进程父进程退出pid_tpidfork();if(pid0)exit(0);// 2. 子进程脱离终端setsid创建新会话setsid();// 3. 再次fork防止重新获取终端pidfork();if(pid0)exit(0);// 4. 改变工作目录chdir(/);// 5. 重设文件权限掩码umask(0);// 6. 关闭标准输入输出close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 守护进程主体...while(1){// do somethingsleep(1);}return0;}二、信号机制2.1 信号是什么信号Signal是Linux中进程间通信的最简单方式——内核或其他进程向目标进程发送一个异步通知。你可以理解为软件中断。2.2 常用信号一览信号编号名称默认行为能否捕获SIGHUP1挂起终止能SIGINT2中断(CtrlC)终止能SIGQUIT3退出(Ctrl)终止核心转储能SIGKILL9强制杀死终止不能SIGSEGV11段错误终止核心转储能SIGTERM15优雅终止终止能SIGSTOP19暂停停止不能SIGCONT18继续恢复运行能SIGCHLD17子进程状态变化忽略能重点SIGKILL(9)和SIGSTOP(19)是两个不能被捕获、阻塞或忽略的信号这是系统留的最后手段。2.3 发送信号# 用kill命令kill-SIGTERM1234# 发送SIGTERMkill-151234# 同上用数字kill-91234# 强制杀死kill-SIGSTOP1234# 暂停进程kill-SIGCONT1234# 恢复进程# 用killall按进程名发信号killall-SIGTERMnginx# 杀掉所有nginx进程killall-9python3# 强杀所有python3# 用pkill按模式匹配pkill-fapp.py# 匹配命令行中包含app.py的进程pkill-uuser# 杀掉user用户的所有进程2.4 键盘快捷键产生信号快捷键信号效果CtrlCSIGINT中断当前前台进程CtrlZSIGSTOP暂停当前前台进程Ctrl\SIGQUIT退出并生成核心转储# CtrlZ暂停后恢复python3 long_running.py# 按CtrlZ# [1] Stopped python3 long_running.pyfg# 恢复到前台运行bg# 恢复到后台运行jobs# 查看被暂停/后台的作业2.5 C语言中的信号处理#includestdio.h#includesignal.h#includeunistd.h// 自定义信号处理函数voidsignal_handler(intsig){printf(Caught signal %d\n,sig);}intmain(){// 注册SIGINT的处理函数CtrlC不再直接退出signal(SIGINT,signal_handler);// 忽略SIGQUITCtrl\无效signal(SIGQUIT,SIG_IGN);printf(PID%d, waiting for signals...\n,getpid());while(1){sleep(1);}return0;}三、管道通信3.1 匿名管道管道是Linux中最古老的进程间通信方式用|表示。它把前一个命令的标准输出连接到后一个命令的标准输入。# 基本用法命令1的输出 → 命令2的输入ls-l/etc|grep.conf# 找出/etc下的conf文件psaux|greppython|grep-vgrep# 找出python进程排除grep自身cat/var/log/syslog|tail-100# 查看最后100行日志dmesg|grep-iusb# 查看USB相关内核日志管道的三个关键特性单向的数据只能从左到右流动每个管道段是独立的子进程管道两边各fork一个子进程数据是流式的数据像水流一样通过不保留在管道中多级管道串联# 找出占用CPU最高的5个进程psaux|sort-k3-rn|head-5# 统计当前目录下各类文件数量ls-la|awk{print $1}|cut-c1|sort|uniq-c# 查看TCP连接数最多的IPnetstat-an|grepESTABLISHED|awk{print $5}|cut-d:-f1|sort|uniq-c|sort-rn|head-103.2 命名管道FIFO匿名管道只能用于有亲缘关系的进程间通信。命名管道也叫FIFO是一个特殊的文件任何进程都可以通过文件路径访问它。# 创建命名管道mkfifo/tmp/myfifo# 终端1写入数据echohello from terminal 1/tmp/myfifo# 终端2读取数据cat/tmp/myfifo# 输出hello from terminal 1# 清理rm/tmp/myfifo特点写入端写入后如果没有读取端写入会阻塞直到有人读取。这保证了数据不会丢失。3.3 管道编程示例#includeunistd.h#includestdio.h#includestring.hintmain(){intfd[2];// fd[0]读端, fd[1]写端pipe(fd);// 创建管道pid_tpidfork();if(pid0){// 子进程读取close(fd[1]);// 关闭写端不需要charbuf[256];read(fd[0],buf,sizeof(buf));printf(Child received: %s\n,buf);close(fd[0]);}else{// 父进程写入close(fd[0]);// 关闭读端不需要char*msgHello from parent!;write(fd[1],msg,strlen(msg)1);close(fd[1]);}return0;}四、IO重定向4.1 标准IO流每个进程启动时自动打开三个文件描述符文件描述符名称默认指向宏0标准输入键盘STDIN_FILENO1标准输出屏幕STDOUT_FILENO2标准错误屏幕STDERR_FILENO4.2 输出重定向# 标准输出重定向echohellofile.txt# 覆盖写入文件不存在则创建echoworldfile.txt# 追加写入# 标准错误重定向ls/nonexistent2error.log# 错误信息写入error.logls/nonexistent2error.log# 错误信息追加# 标准输出和错误分别重定向ls/etc /nonexistentout.log2err.log# 标准输出和错误重定向到同一文件ls/etc /nonexistentall.log21# 注意顺序先重定向stdout再把stderr指向stdoutls/etc /nonexistentall.log# 简写形式bash支持易错点21 file.txt和 file.txt 21效果不同前者先把stderr指向当前stdout屏幕再把stdout重定向到文件结果stderr仍然输出到屏幕。后者才是正确的全部重定向到文件。4.3 输入重定向# 从文件读取输入wc-l/etc/passwd# 统计passwd文件的行数# Here Document内联输入catEOF 第一行 第二行 EOF# Here Stringgreprootroot:x:0:0:root:/root:/bin/bash4.4 /dev/null黑洞设备# 丢弃输出测试命令是否报错但不需要输出内容ls/etc/dev/null21# 清空文件内容cat/dev/null/var/log/app.log# 只关心命令执行是否成功ifping-c1192.168.1.1/dev/null21;thenecho网络通elseecho网络不通fi4.5 tee命令双向输出tee把输入同时输出到屏幕和文件相当于管道中的一个三通接头# 编译时同时看输出和保存日志make21|teebuild.log# 追加模式make21|tee-abuild.log4.6 进程替换# 把命令的输出当作临时文件使用diff(lsdir1)(lsdir2)# 比较两个目录的内容差异# (...) 创建一个临时命名管道# 同时读取两个命令的输出cat(echofirst)(echosecond)4.7 文件描述符操作# 打开文件描述符exec3/tmp/fd3.log# 打开fd3写入/tmp/fd3.logechowrite to fd33# 向fd3写入exec3-# 关闭fd3# 备份和恢复标准输出exec31# 把fd3指向stdout备份exec1/tmp/log.txt# 重定向stdout到文件echothis goes to file# 写入文件exec13# 恢复stdoutechothis goes to screen# 写入屏幕五、综合实战嵌入式编译日志分析# 完整的编译流程编译、记录日志、分析错误#!/bin/bashLOGbuild_$(date%Y%m%d_%H%M%S).log# 编译同时输出到屏幕和日志文件make21|tee$LOG# 统计编译结果ERRORS$(grep-cerror:$LOG)WARNINGS$(grep-cwarning:$LOG)echoecho编译完成错误$ERRORS个警告$WARNINGS个echo日志已保存到$LOG# 如果有错误只显示错误部分if[$ERRORS-gt0];thenecho--- 错误详情 ---greperror:$LOGexit1fi六、总结进程是Linux系统运行的单元信号是进程间最快的通信方式管道让命令可以链式协作IO重定向让程序的输入输出可以灵活操控。这四个概念组合在一起就是Shell编程强大表达力的来源——用简单的命令组合出复杂的数据处理流水线。嵌入式开发中编译日志分析、自动化构建脚本、设备数据采集都离不开这些基础。