Linux 进程编程
Linux 进程与线程
一个a.out的虚拟内存图
MMU(虚拟内存映射, 是硬件, 在CPU内部)
-
功能
- 将用户空间的虚拟地址映射为物理地址
- 设计内存的访问级别(一块物理内存的访问级别不是根据其位置, 而是根据MMU的设置)
-
特点
- 因为一个a.out的用户空间的代码是相互独立的, 所以运行两个a.out进程他们在用户空间上彼此独立的, 但是内核空间上的PCB确实共享的, 所以他们可以通过PCB进行通信, 注意: 这两个进程来自于同一个可执行文件, 不同的可执行文件他们的PCB不可能是共用的, 但是他们在虚拟内存地址上是完全独立的
PCB
- id
- status
- 当前工作目录
- 虚拟地址信息
- 使用到的寄存器
- umask掩码
- 和信号相关的信息
- uid和gid
- session和进程组
- 资源使用上限: ulimit -a显示当前进程的资源上限
- 文件描述符, 一个file struct数组
环境变量
- 直接获取环境变量, extern char **environ; 注意: 必须使用extern修饰
- getenv
- setenv
- unsetenv
关于进程函数
- fork
- 使用fork函数让一个父进程fork多个子进程的时候要注意在恰当的时候break和sleep达到效果
- wait
- waitpid
孤儿进程与僵尸进程
-
孤儿进程
- 父进程结束, 子进程还是执行, 则子进程的ppid会成为1, 让init进程成为子进程的父进程, 让init进程管理该子进程
-
僵尸进程
- 父进程还在执行, 子进程已经结束, 但是父进程没有调用wait或者waitpid函数回收子进程的PCB。使用ps ajx命令查看发现子进程已经成为了Z+进程, 如果要回收该子进程不能对其发送kill信号, 因为子进程已经结束了, 要对其父进程发送kill信号, 让其成为孤儿进程, 让init进程回收它的PCB资源
exec族函数
-
int execl(const char *path, const char *arg, ...), l表示list, 指的是参数列表
-
int execlp(const char *path, const char *arg, ...), 执行程序的时候会在PATH路径中找可执行文件, 可以不加路径, p表示path
- path为文件名
- arg: 参数, 第一个需要是命令名占位, 和path是一样的值
- 可变参数结束使用NULL占位
-
特点:
- 在Linux中, 调用exec函数直接将当前的a.out的.data与.text替换成指定命令的.text和.data, 也就是将当前的进程的代码与数据替换为目标程序, 所以exec的返回值我们无需关心, 因为只要exec族函数执行成功, 我们写的程序的代码段就已经别替换成了其他的代码, 我们代码中exec族函数之后的语句就不能执行了, 为了能够执行之后的代码, exec常与fork函数配合使用, 将子进程去执行exec, 再让父进程去wait或者waitpid回收子进程的PCB资源, 获取子进程的退出原因等
wait函数
-
pid_t wait(int *status): status是返回值, 成功返回ID, 失败返回-1
- 参数
- status是wait参数的返回值, wait会将子进程退出的原因保存在status执行的内存空间中
- 返回值pid_t是子进程的pid
- 功能
- 阻塞父进程
- 回收子进程资源
- 得到子进程退出的原因
- 参数
-
pid_t waitpid(pid_t pid, int *status, options)
- 常用搭配
- waitpid(-1, NULL, 0), 与wait(NULL)的效果一模一样
- waitpid(-1, NULL, WNOHANG), 类似于Linux五种IO模型一样, 父进程一之星waitpid函数, 检查子进程是否退出, 如果没有退出, 则立即返回继续执行父进程的代码, 强烈建议使用while循环执行waitpid(-1, NULL, WNOHANG), 因为子进程结束之后不会通知父进程, 父进程需要不断地检查子进程是否已经退出了, 如果退出了则返回子进程的pid, 如果父进程回收了所有的子进程, 则返回-1
- 常用搭配
IPC(进程间通讯)
管道
- 内核通过缓冲区实现(是一个环形队列), 是一个匿名伪文件
- 是有父子关系或者兄弟关系的进程之间才存在, 父子与兄弟关系需要通过fork函数构建出来
- 管道是单向的
使用pipe(使用方便)
-
函数
- int pipe(int pd[2]);
- pd[0]->read, pd[1]->write
- int pipe(int pd[2]);
-
两个进程中, 其中一个进程读取管道
- 在管道的写端开始的时候, 则在读取的时候如果管道恰巧没有数据就会read阻塞, 如果写端关闭则read直接返回0
- 在向一个管道进行写时, 如果另一端关闭了读则程序直接被内核关闭
- 当管道满时, 写端就会阻塞
-
使用pipe函数构建父子关系, 调用ls -l | grep a.out
-
demo
int main() { int fd[2]; if (-1 == pipe(fd)) { perror("pipe error"); } int i = 0; pid_t pid = 0; for (i = 0; i < 2; ++i) { pid = fork(); if (-1 == pid) { perror("fork error"); } else if (pid == 0) { printf("I am a child process, pid: %d, ppid: %d\n", getpid(), getppid()); break; } } if (i == 0) { close(fd[0]); dup2(fd[1], STDOUT_FILENO); execlp("ls", "ls", "-l", NULL); close(fd[1]); } else if (i == 1) { close(fd[1]); dup2(fd[0], STDIN_FILENO); execlp("grep", "grep", "a.out", NULL); close(fd[0]); } else { close(fd[0]); close(fd[1]); /* while (-1 != waitpid(-1, NULL, 0)) { printf("kill one\n"); } */ while (-1 != waitpid(-1, NULL, WNOHANG)) { printf("...\n"); } printf("over\n"); } return 0;
}
### 有名管道(fifo[first in, first out]) + fifo是一个伪文件系统, 伪文件不在磁盘上, 而是在内核的缓冲区中 + 使用命令或者函数mkfifo可以创建一个伪文件 + 对于为文件我们执行一般的文件的操作即可, 只不过可以有两个不同的进程通过该伪文件进行交互 + demo * write文件(先启动write程序, 在启动read程序) ```c int main() { int fd = open("testfifo", O_RDWR); if (-1 == fd) { perror("open error"); exit(-1); } int i = 0; char buf[256]; memset(buf, 0x00, sizeof(buf)); while (1) { sprintf(buf, "this is %d\n", i++); write(fd, buf, strlen(buf)); } return 0; }
* read ```c int main() { int fd = open("testfifo", O_RDONLY); if (-1 == fd) { perror("open error"); exit(-1); } int len = 0; char buf[256]; memset(buf, 0x00, sizeof(buf)); while (0 != (len = read(fd, buf, sizeof(buf)))) { sleep(1); write(STDOUT_FILENO, buf, len); } return 0; } ```
文件映射共享IO(mmap)
-
mmap将磁盘上的文件映射到内存中的某一块区域, 程序只需要对该内存空间进行操作就可以达到对文件进行操作的目的, mmap是进程间中最快的
-
注意点
- mmap申请的内存大小不能大于原始文件的大小, 否则会报 bus error
- mmap返回的是一个泛型执行, 记得转型
- 用户为mmap申请的空间不够写了, 只要还控制在原始文件大小只能, mmap会自动扩容
-
函数
- void * mmap( void *, size_t, int, int, int, off_t )
- offset: 必须是4k倍数
- void * mmap( void *, size_t, int, int, int, off_t )
-
匿名映射
- 在传入到mmap中的参数中, 使用MAP_ANON | MAP_SHARED或者MAP_ANON | MAP_PRIVATE, fd传入-1, 偏移量会被忽略掉
- 匿名映射的应用, 可以用于父子进程通信, 兄弟进程通信
- demo
int main() { int *mem = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0); if (MAP_FAILED == mem) { perror("匿名mmap函数调用失败!"); exit(-1); } pid_t pid = fork(); if (pid == 0) { // 子进程 *mem = 100; printf("子进程*mem0的值为: %d\n", *mem); sleep(1); printf("子进程*mem1的值为: %d\n", *mem); } else if (pid > 0) { printf("父进程中的*mem0的值为: %d\n", *mem); *mem = 1001; printf("父进程中的*mem1的值为: %d\n", *mem); sleep(3); wait(NULL); } else { perror("错误异常"); } munmap(mem, 4); return 0; }
-
两个进程之间通信
-
write
int main() { int fd = open("./connect.link", O_RDWR); int *mem = (int *)mmap(NULL, 4, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0); if (MAP_FAILED == mem) { perror("在文件mmap_anon.c文件mmap函数调用错误"); exit(-1); } // 向匿名文件中写数据 *mem = 100; printf("我是mmap_anon.c文件, mem的值为%d\n", *mem); while (1) { sleep(1); } return 0; } - read
int main() {
int fd = open("./connect.link", O_RDWR);
int *mem = (int *)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == mem) {
perror("在文件mmap_anon_r.c文件中匿名函数调用错误");
exit(-1);
}
printf("读取到的内存共享IO区域的值为: %d", *mem);
munmap(mem, 4);
return 0;
}``` -
信号
-
在特定的条件下发生, 携带的信号量少
-
信号产生:
- 键盘: C-z, C-c
- 函数: kill, raise, abort
- 定时器: alarm, setitemer
- 命令产生: kill
- 硬件异常, 段错误, 浮点错误, 段错误
-
信号状态
- 产生
- 递达: 信号被处理了
- 未决: 信号被阻塞了
-
信号处理
- 忽略
- 执行默认操作
- 铺获
-
信号要素
- 编号
- 事件名
- 名称
- 默认动作
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· Windows桌面应用自动更新解决方案SharpUpdater5发布
· 我的家庭实验室服务器集群硬件清单
· C# 13 中的新增功能实操
· Supergateway:MCP服务器的远程调试与集成工具
· Vue3封装支持Base64导出的电子签名组件