Linux多进程
进程基础
## 什么是进程
进程定义:进程是操作系统中的一次执行过程,它是操作系统进行资源分配和调度的基本单位;
只要用户执行了一个程序,在内核空间就会创建一个 task_struct 结构体,这个结构体就代表当前的进程,进程运行产生的所有的信息 (如进程状态、PID、内存指针等) 都被放到这个结构体中保存着;
进程执行完之后,task_struct 会被内核回收;如果异常崩溃,内核也会清理资源 (避免僵尸进程);
## 进程的特征
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的;
并发性:任何进程都可以同其他进程一起并发执行;
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程各自独立、不可预知的速度向前推进;
## 进程的组成
进程是由三个部分组成的,分别为进程控制块 (PCB)、文本段、数据段;
进程控制块 (PCB,Process Control Block):PCB 是操作系统用于管理和控制进程的核心数据结构,每个进程在内存中都有唯一的 PCB,它记录着进程的所有关键信息 (如进程识别信息、进程状态信息、内存管理信息、资源占用信息等),是操作系统感知进程存在的依据;
文本段:是进程中存储可执行指令的内存区域,即程序编译后生成的机器码;
为了防止程序意外修改自身指令,文本段通常被设置为只读;如果多个进程运行同一个程序,他们可以共享同一份文本段,节省内存空间;编译后的指令数量是固定的,文本段的大小在进程运行期间不会改变;
数据段:是用于存储进程运行过程中需要的数据;根据数据 “初始化状态” 和 “可修改性”,分为初始化数据段、未初始化数据段、堆、栈;
## Linux系统进程类型
Linux 系统下有三种进程类型,分别是交互进程 、批处理进程和守护进程;
交互进程:
定义:由 shell 维护,需要用户在终端手动启动的进程,通过 shell 和用户进行交互 (用户交互输入 / 输出);
特点:依赖终端环境,通常在前台运行,生命周期与用户操作相关;
批处理进程:
定义:无需用户实时交互,优先级比较低,按预设任务序列自动运行的进程,通常在后台执行;
特点:优先级较低 (避免占用关键资源),多通过脚本或任务调度工具启动;
守护进程:
定义:系统启动时自动运行的后台进程,独立于终端,通常随系统启动而启动,随系统关闭而终止,用于提供持续服务;
特点:运行时脱离终端;
## 进程的状态
在 Linux 系统中,进程会因资源占用、执行阶段不同,呈现多种状态;
运行状态 ®:也称之为可运行状态,就是处于运行状态的进程,正在 CPU 执行或等待 CPU 调度;
可中断睡眠状态 (S):进程在等待某个事件 (如等待 I/O 操作、等待特定信号等) 完成,可被信号唤醒;
不可以中断睡眠状态 (D):进程正在等待关键硬件操作完成,不能被信号中断,确保数据完整性;
停止状态 (T):进程被暂停执行,通常是收到了特定的信号 (如 SIGSTOP (暂停进程)) 或者在调试程序时被调试器暂停,可被信号恢复执行 (如 SIGCONT);
僵尸状态 (Z):进程已终止,但资源未被回收,等待父进程进行回收;
死亡态 (X):进程彻底终止,资源全部释放,是一个瞬间状态,几乎无法观测到;

## 进程的附加状态
在 Linux 系统中,进程除了基础状态之后,还会有一些附加状态标记,用于补充说明进程的特殊属性;
+(前台进程组):表示进程属于前台进程组,与当前终端直接关联,用户可以通过终端输入直接控制;
l(多线程进程):表示进程包含线程 (LWP),通常对应多线程程序;
N(低优先级进程):表示进程的 nice 值为正数 (NI> 0),优先级较低,CPU 资源优先给其他进程;
<(高优先级进程):表示进程的 nice 值为负数 (NI < 0),优先级较高,会优先获取 CPU 资源;
s(会话 leader 进程):表示进程是会话首进程,负责管理一个会话中的其他进程;
L(进程持有锁):表示进程正在持有某个文件锁或者是资源锁,可能在等待其他进程释放锁资源;
## 特殊PID
0号:
进程名称:idle
作用: 系统启动后的第一个进程,负责空闲CPU调度,无其他进程运行时,idle进程占用CPU
1号
进程名称:init
作用:由0号进程创建,负责初始化系统硬件,启动服务,回收孤儿进程
2号
进程名称:kthreadd
作用:负责内核进程调度
## PS命令
功能:用于查看当前系统进程的快照信息,包括进程的PID,所属用户,占用CPU和内存资源情况等
## top命令
功能:动态显示系统中各个进程的资源使用情况,可以实时查看进程的CPU使用率,内存使用量,线程数等,并且能按照不同的指标进行排序
## htop命令
功能:是一个交互式的进程查看工具,相比top具有更友好的用户界面,支持鼠标操作;可以更加直观地展示进程树结构
## kill命令
功能:用于向进程发送信号,最常见地就是用来进程。通过指定进程的PID,发送特定的信号让进程执行相应的操作,比如终止,暂停等等;
## killall命令
功能:根据进程名来终止进程,当系统中有多个同名进程需要同时终止时,killall非常方便
## pstree命令
功能:以树结构显示进程之间的父子关系,从系统初始化init开始,直观地展示整个系统进程的层次结构
## bg/fg命令
## nice/renice命令
进程相关函数
## 进程的创建
int main (int argc, const char* argv[])
{
pid_t pid=0
pid=fork();
if(pid =-1)
{
perror("创建子进程失败");
return -1;
}
else if(pid == 0)
{
//子进程
}
else if(pid > 0)
{
//父进程
}
return 0;
}
为什么要这样设计返回值?
因为任何子进程都只有一个父进程,而且子进程总是可以通过getppid()来获取父进程的pid,相反一个父进程可以有多个子进程,而且无法获取各子进程ID,如果父进程想跟踪所有的子进程,需要记录每次调用fork的返回值
父子进程内存空间
1.父进程在fork产生子进程时,用到了写时拷贝原则
2.如果父子进程中对内存进行读操作,那么内存不会被重新映射物理内存
3.如果父子进程中的任意一方对内存有写操作时,才会重新映射到新的物理内存
## 进程号获取函数

int main (int argc, const char* argv[])
{
pid_t pid=0
pid=fork();
if(pid =-1)
{
perror("创建子进程失败");
return -1;
}
else if(pid == 0)
{
//子进程
printf("我的pid是:%d \n",getpid());
printf("我的父进程的pid是:%d \n",getppid());
}
else if(pid > 0)
{
//父进程
printf("我的pid是:%d \n",getpid());
printf("我的父进程的pid是:%d \n",getppid());
}
return 0;
}
## 进程退出函数


## 资源回收函数




特殊进程
## 孤儿进程
定义:当一个进程的父进程先于它终止,该进程就会成为孤儿进程;
产生原因:父进程意外崩溃、被强制终止 (如 kill -9),或父进程主动退出但未正确处理子进程;
特点:
失去父进程后,会被系统的 “祖先进程”(linux 中是 init 或 systemd,PID=1) 收养;
仍能正常运行,完成自身任务后会正常终止,不会占用额外资源;
影响:
本身是系统正常处理机制的结果,无负面影响,反而避免了进程成为 “无主进程”;
例如:若父进程因 bug 崩溃,子进程被 init 收养后可继续提供服务;
## 僵尸进程
定义:进程终止后,内核未释放其进程控制块 (PCB),该进程状态变为 Z (僵尸态),成为僵尸进程;
产生原因:父进程未调用 wait () 或 waitpid () 等系统调用,读取子进程的退出状态 (如退出码、终止原因),导致内核无法回收 PCB;
特点:
已终止运行,不再执行任何代码,但 PCB 仍占用内存资源 (如 PID、退出状态);
通过 ps 命令中显示状态为 Z 或者是 Z+(僵尸态),名称可能为;
影响:
系统 PID 数量有限,大量僵尸进程会耗尽 PID 资源,导致新进程进行无法创建;
不占用用户空间的内存数据段,但内核空间的 PCB 资源仍被占用且无法使用;
处理方式:
预防:父进程调用 wait ()/waitpid () 主动回收子进程状态;
清除:kill -9 无法直接杀死僵尸进程 (其已终止,无运行实体);
父进程未处理,可终止父进程 (僵尸进程被 1 号进程收养,1 号进程会定期回收其 PCB);
## 守护进程
定义:运行在后台的特殊进程,独立于控制终端,用于持续提供系统服务;
设计目的:不受用户登录 / 注销影响,长期稳定运行 (如网络服务、定时任务);
特点:
脱离终端:无控制终端,避免终端关闭导致进程终止;
后台运行:通过 ps 命令查看状态通常为 S (休眠),名称多以 d 结尾 (如 httpd、systemd);
父进程为 1 号进程:启动后会与原本父进程脱离,被 1 号进程收养;
环境干净:默认不继承终端信号,工作目录通常为 /,文件描述符 (如 stdin/stdout/stderr) 被关闭或重定向到 /dev/null;
守护进程创建流程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
//创建守护进程
//用kill -9 pid 杀死该进程 kill -15 pid 正常结束进程
int main(int argc,const char* argv[])
{
pid_t pid=fork();
if( pid < 0 )
{
perror("创建子进程失败");
return -1;
}
else if ( pid == 0 )
{
printf("创建子进程成功,子进程pid为: %d \n",getpid());
pid_t setsid_pid = setsid();
if( setsid_pid == -1 )
{
perror("创建新会话失败");
return -1;
}
printf("创建新会话成功\n");
int chdir_pid=0;
chdir_pid=chdir("/");
if(chdir_pid == -1)
{
perror("修改路径失败");
return -1;
}
mode_t umask_pid = umask(0022);
printf("之前的掩码为:%d \n", umask_pid );
// 关闭所有从父进程继承过来的文件描述符的集合
for( int i=3 ; i < 1024 ; i++)
{
close(i);
}
// 将stdin,stdout,stderr重定向到文件中
int fd = -1;
fd = open("yang.log",O_RDWR | O_CREAT | O_TRUNC,0666);
if( fd == -1 )
{
perror("打开文件失败");
return -1;
}
printf("打开文件yang.log成功\n");
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
//开启自己服务
while(1)
{
printf("HELLO WORLD\n");
}
}
else if( pid > 0 )
{
printf("创建父进程成功,父进程pid为: %d \n",getpid());
//父进程退出,子进程成为孤儿进程
exit(0);
}
return 0;
}




进程间通信
概述
Unix 平台进程通信方式 (早期):
AT&T 的贝尔实验室,对 Unix 早期的进程间通信进行了改进和扩充,形成了 “system V IPC”,这类机制最初设计聚焦单机内进程通信;
BSD (加州大学伯克利分校的伯克利软件发布中心) 跳过了该限制,形成了基于套接字 (socket) 的进程间通信机制;
Linux 继承了上述所有的通信方式,常用的进程间通信方式:
传统的进程间通信方式:无名管道 (pipe)、有名管道 (fifo) 和信号 (signal)
System V IPC 对象:
共享内存 (shared memory)、消息队列 (message queue) 和信号量 (semaphore)
BSD:套接字 (socket)
## 无名管道

注意:pipefd【2】数组中拿到的是读端和写端的文件描述符
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc,const char *argv[])
{
//1.创建无名管道
//无名管道是单向的,支持半双工通信方式
//一端用于读(pipefd[0]).一端用于写(pipefd[1])
int fd[2] = {0};
int pipe_ret = 0;
if(-1 == (pipe_ret = pipe(fd)))
{
perror("创建无名管道失败");
return -1;
}
//注意1.pipe的文件描述符跟open函数的文件描述符使用的是同一组
//注意2.fd[0]表示的是读端,fd[1]表示的是写端
printf("创建无名管道成功,fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
//2.创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("创建子进程失败");
return -1;
}
else if(pid == 0)
{
//子进程
//子进程接收父进程的数据 读数据
//关闭子进程的写端
if(-1 == (close(fd[1])))
{
perror("关闭子进程写端失败");
return -1;
}
printf("关闭子进程写端成功\n");
//从无名管道中读取数据
char buf_str[BUFSIZ] = {0};
ssize_t read_ret = 0;
if(-1 == (read_ret = read(fd[0],buf_str,sizeof(buf_str))))
{
perror("从无名管道中读取数据失败");
return -1;
}
printf("从无名管道中读取数据成功,读取到的数据为:\n");
printf("%s\n",buf_str);
//关闭子进程的读端
if(-1 == (close(fd[0])))
{
perror("关闭子进程读端失败");
return -1;
}
printf("关闭子进程读端成功\n");
//子进程退出
exit(EXIT_SUCCESS);
}
else if(pid > 0 )
{
//父进程
//父进程给子进程发送数据 写数据
//关闭父进程的读端
int close_ret = 0;
if(-1 == (close_ret = close(fd[0])))
{
perror("关闭父进程读端失败");
return -1;
}
printf("关闭父进程读端成功\n");
//往无名管道中写数据
char *str = "hello world";
ssize_t write_ret = 0;
if(-1 == (write_ret = write(fd[1],str,strlen(str))))
{
perror("往无名管道中写数据失败");
return -1;
}
printf("往无名管道中写数据成功\n");
//关闭父进程的写端
if(-1 == (close(fd[1])))
{
perror("关闭父进程写端失败");
return -1;
}
printf("关闭父进程写端成功,已将数据写入无名管道中\n");
//等待子进程退出
wait(NULL); //不关注子进程的退出状态
}
return 0;
}

# 有名管道



/*
有名管道在使用的时候必须要保证读端和写端都被打开,open操作才会返回
有名管道一般不用于亲缘关系进程之间的通信
有名管道一般用于非亲缘关系进程之间的通信
有名管道中我们必须要调用unlink函数
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, const char *argv[])
{
// 1.对输入参数的参数个数进行检查
if (argc != 2)
{
printf("输入参数的参数个数有误\n");
printf("usage: ./a.out filename\n");
return -1;
}
// 2.创建有名管道 mkfifo函数
// 如果管道文件存在,则会进行报错
if (-1 == (mkfifo(argv[1], 0664)))
{
perror("创建有名管道失败");
return -1;
}
printf("创建有名管道成功\n");
// 3.创建子进程 fork()
pid_t pid = fork();
if (pid == -1)
{
perror("创建子进程失败");
return -1;
}
else if (pid == 0)
{
// 子进程
// 打开管道文件
// 在子进程中以只写的方式打开管道文件
// 内核管道缓冲区是在第一次open函数调用(打开管道文件)的时候才会进行分配
int fd = open(argv[1], O_WRONLY);
if (fd == -1)
{
perror("子进程打开管道文件失败");
unlink(argv[1]);
return -1;
}
printf("子进程打开管道文件成功\n");
// 往管道文件中写入数据
char buf[] = "hello world";
ssize_t ret_write = 0;
if (-1 == (ret_write = write(fd, buf, sizeof(buf))))
{
perror("子进程往管道文件中写入数据失败");
return -1;
}
printf("子进程往管道文件中写入数据成功\n");
// 关闭管道文件
if (-1 == (close(fd)))
{
perror("子进程关闭管道文件失败");
return -1;
}
printf("子进程关闭管道文件成功\n");
// 让子进程进行退出
exit(EXIT_SUCCESS); // 正常退出
}
else if (pid > 0)
{
// 父进程
// 打开管道文件
// 管道文件以只读的方式打开文件
int fd = open(argv[1], O_RDONLY);
if (fd == -1)
{
perror("父进程打开管道文件失败");
unlink(argv[1]); // 清理有名管道
return -1;
}
printf("父进程打开管道文件成功\n");
// 针对于unlink函数仅仅是删除文件系统中的"文件名链接"
//不会影响已经打开的文件描述符
//已经打开的文件描述符还可以正确的从管道文件中进行读写操作
//可以避免误删文件名或者是提前删除文件名导致对管道文件中的数据
//读写操作失败
// if(-1 == (unlink(argv[1])))
// {
// perror("unlink error\n");
// return -1;
// }
// printf("unlink ok\n");
// 等待子进程退出
wait(NULL); // 不关注子进程的退出状态值
// 从管道文件中读取数据
char buf[BUFSIZ] = {0};
ssize_t ret_read = 0;
if (-1 == (ret_read = read(fd, buf, sizeof(buf))))
{
perror("父进程从管道文件中读取数据失败");
return -1;
}
printf("父进程从管道文件中读取数据成功\n");
printf("读取到的数据为:%s\n", buf);
// 关闭管道文件
if (-1 == (close(fd)))
{
perror("父进程关闭管道文件失败");
return -1;
}
printf("父进程关闭管道文件成功\n");
// 自动清理有名管道
if (-1 == (unlink(argv[1])))
{
perror("自动清理有名管道失败");
return -1;
}
printf("自动清理有名管道成功\n");
//不要重复使用unlink函数
// if (-1 == (unlink(argv[1])))
// {
// perror("自动清理有名管道失败");
// return -1;
// }
// printf("自动清理有名管道成功\n");
}
return 0;
}


## 信号
信号是中断的一种软件模拟,它是基于 linux 内核实现的;
用户可以给进程发信号,进程可以给进程发信号,linux 内核也可以给进程发信号;
对信号处理的三种方式:默认、忽略、捕捉;
常用信号





//模拟kill命令:kill -信号编号 进程号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,const char *argv[])
{
//1.对输入参数的参数个数进行检查
//使用方式:./3-kill -信号编号 进程号
if(argc != 3 || argv[1][0] != '-')
{
printf("输入的参数个数有误\n");
printf("usage:./3-kill -信号编号 进程号\n");
return -1;
}
//2.将参数转换为整数(将字符串转换为整数) atoi函数
int signo = atoi(argv[1]+1);
//信号一共有64个,是从1信号开始的
if(signo < 1 || signo > 64)
{
printf("输入的信号编号有误\n");
return -1;
}
int pid = atoi(argv[2]);
if(pid <= 0 )
{
printf("输入的进程号有误\n");
return -1;
}
//3.使用kill函数发送信号
//第一个参数要发送信号的目标
//第二个参数要发送的信号的编号
if(-1 == (kill(pid,signo)))
{
perror("使用kill函数发送信号失败");
return -1;
}
printf("使用kill函数发送信号成功\n");
//4.打印结果
printf("kill -%d %d\n",signo,pid);
return 0;
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)