简介

进程是系统资源分配的最小单位,它曾经也是CPU调度的最小单位,但后面被线程所取代。

进程树

Linux系统通过父子进程关系串联起来,所有进程之前构成了一个多叉树结构。

孤儿进程

孤儿进程是指父进程已经结束,子进程还在执行的进程。那么此时此刻,该进程就变成了孤儿进程。

当进程变成孤儿进程后,系统会认领该进程,并为他再分配一个父进程(就近原则,爸爸的爸爸,爸爸的爸爸的爸爸)。

》当孤儿进程被认领后,就很难再进行标准准入输出对其停止了。因为切断了跟终端的联系,所以编码过程中要尽量避免出现孤儿进程

进程通讯(inter -Process Communication,IPC)

多个进程之间的内存相互隔离,多个进程之间通讯方式有如下几种:

管道(pipe)

匿名管道

半双工通信,即数据只能在一个方向上流动。只能在具有父子关系的进程之间使用。

其原理为:内核在内存中创建一个缓冲区,写入端将数据写入缓冲区,读取端从缓冲区中读取数据

点击查看代码
#include <stdio.h>
#include <unistd.h>
#include <string.h> int main() {
int pipefd[2];
pid_t pid;
char buffer[100]; // 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
} // 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} if (pid == 0) {
// 子进程:关闭写端,从管道读取数据
close(pipefd[1]);
read(pipefd[0], buffer, sizeof(buffer));
printf("Child process received: %s\n", buffer);
close(pipefd[0]);
} else {
// 父进程:关闭读端,向管道写入数据
close(pipefd[0]);
const char *message = "Hello, child process!";
write(pipefd[1], message, strlen(message) + 1);
close(pipefd[1]);
} return 0;
}

命名管道

可以在任意两个进程之间进行通信,不要求进程具有亲缘关系;遵循先进先出(FIFO)原则。

其原理为:在文件系统中创建一个特殊的文件,进程通过读写这个文件来进行通信,因此文件读取是从头开始读,所以先进先出。

点击查看代码
// 写进程
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> #define FIFO_NAME "myfifo" int main() {
int fd;
const char *message = "Hello, named pipe!"; // 创建命名管道
mkfifo(FIFO_NAME, 0666); // 打开命名管道进行写操作
fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("open");
return 1;
} // 向命名管道写入数据
write(fd, message, strlen(message) + 1);
close(fd); return 0;
} // 读进程
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h> #define FIFO_NAME "myfifo" int main() {
int fd;
char buffer[100]; // 打开命名管道进行读操作
fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
} // 从命名管道读取数据
read(fd, buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
close(fd); // 删除命名管道
// 理论上,命名管道是可以重复使用的,其本质就是操作文件而已。只是不建议这么操作。
unlink(FIFO_NAME); return 0;
}

共享内存(Shared Memory)

多个进程共享同一块内存地址,是最快的IPC方式。

其原理为:内核在物理内存中分配一块内存区域,多个进程将内存地址映射到自己的虚拟空间内,从而可以直接读写该区域。

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h> int main(int argc, char const *argv[])
{
//创建一个共享内存对象
char shm_name[100]={0};
sprintf(shm_name,"/letter%d",getpid());
int fd= shm_open(shm_name,O_RDWR|O_CREAT,0664);
if(fd<0){
perror("shm_open");
exit(EXIT_FAILURE);
} //给共享内存对象设置大小
ftruncate(fd,1024); //内存映射
char *share= mmap(NULL,1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(share==MAP_FAILED){
perror("mmap");
exit(EXIT_FAILURE);
} //映射完成,关闭fd连接
close(fd); //使用内存,完成进程通讯
pid_t pid=fork();
if(pid<0){
perror("fork");
exit(EXIT_FAILURE);
} if(pid==0){
//子进程
strcpy(share,"子进程,嘿嘿嘿嘿。");
}
else{
//父进程
waitpid(pid,NULL,0);
printf("收到子进程的信息:%s",share); } //释放映射
munmap(share,1024); //释放共享内存对象
shm_unlink(shm_name);
return 0;
}

临时文件系统

linux的临时文件系统是一种基于内存的文件系统,它将数据存储在RAM中或者SWAP中,共享对象同样也是挂在在临时文件系统中。

我们可以写一段不释放的代码,来眼见为实。

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h> int main(int argc, char const *argv[])
{
char sh_name[100]={0};
sprintf(sh_name,"/letter%d",getpid());
int fd=shm_open(sh_name,O_CREAT,0664);
if(fd<0){
perror("shm open");
exit(EXIT_FAILURE);
}
while (1)
{
//代码空转,方便查看内存区域。
} return 0;
}

代码执行中:生成的临时文件

执行后,临时文件也不会消失,因为没有释放。

消息队列(message queue)

消息队列是消息的链表,存放在内核中,由消息队列标识符标识。进程可以向队列中添加消息,也可以从队列中读取消息。

其原理为:内核为每个消息队列维护一个消息链表,消息可以按照不同的类型进行分类,进程可以根据消息类型有选择地读取消息。

生产者
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h> int main()
{
struct mq_attr attr;
attr.mq_flags=0;
attr.mq_maxmsg=10;
attr.mq_msgsize=100;
attr.mq_flags=0;
attr.mq_curmsgs; struct timespec time_info; //创建消息队列
char * mq_name="/p_c_mq";
mqd_t mqdes=mq_open(mq_name,O_RDWR|O_CREAT,0664,&attr);
if(mqdes==(mqd_t)-1){
perror("mq_open");
exit(EXIT_FAILURE);
}
//从控制台接受数据,并发送给消费者
char write_buff[100];
while (1)
{
memset(write_buff,0,100);
ssize_t read_count= read(STDIN_FILENO,write_buff,100);
clock_gettime(0,&time_info);
time_info.tv_sec+=5; if(read_count==-1){
perror("read");
continue;
}
else if(read_count==0)
{
printf("控制台停止发送消息");
char eof=EOF;
if(mq_timedsend(mqdes,&eof,1,0,&time_info)==-1){
perror("mq_timedsend");
}
break;
}
else{
if(mq_timedsend(mqdes,write_buff,strlen(write_buff),0,&time_info)==-1){
perror("mq_timedsend");
}
printf("从命令行接受到数据,并发送给消息的队列。");
}
}
//关闭资源
close(mqdes);
//由消费者关闭更加合适,否则消费者可能无法接收到最后一条消息。
//unlink(mq_unlink);
return 0;
}
消费者
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h> int main()
{
struct mq_attr attr;
attr.mq_flags=0;
attr.mq_maxmsg=10;
attr.mq_msgsize=100;
attr.mq_flags=0;
attr.mq_curmsgs; struct timespec time_info; //创建消息队列
char * mq_name="/p_c_mq";
mqd_t mqdes=mq_open(mq_name,O_RDWR|O_CREAT,0664,&attr);
if(mqdes==(mqd_t)-1){
perror("mq_open");
exit(EXIT_FAILURE);
}
//从控制台接受数据,并发送给消费者
char read_buff[100];
while (1)
{
memset(read_buff,0,100);
clock_gettime(0,&time_info);
time_info.tv_sec+=5;
//读取数据
if(mq_timedreceive(mqdes,read_buff,100,NULL,&time_info)==-1){
perror("mq_timedreceive");
}
//判断是否结束
else if(read_buff[0]==EOF){
printf("接收到结束信息,准备退出....");
break;
}
else{
printf("接受到来自生产者的信息%s\n",read_buff);
}
}
//关闭资源
close(mqdes);
unlink(mq_unlink);
return 0;
}

临时文件

与内存共享类似,Linux底层也会生成一个临时文件来代表队列。

三者之间的演化关系

在操作系统的发展过程中,最早出现的是管道通信,用于父子进程之间的信息通信

不足:

  1. 单向且受限于亲缘关系限制

    只能在父子进程之间传递,后面又演化出了命名管道,解决了亲缘关系的限制。
  2. 传输效率低

    依赖内核缓冲区,且内核空间有限,无法传输大数据。
  3. 缺乏消息分类与异步支持

然后又演化出共享内存,解决了管道在大数据传输的效率问题。成为现在操作系统中最高效的IPC方式,

不足:

  1. 需要开发者自行处理同步逻辑。

经常听到的Zero Copy也是这个原理。

共享内存同时推出的还有消息队列,它解决了管道/共享内存在功能上的局限性。

消息可以按照分组来选择性的接收,由内核来处理同步逻辑,并对外提供原子操作。

不足:

  1. 性能不如共享内存

    因为消息队列与管道一样,需要从内核复制数据。

消息队列是对管道的功能增强,共享内存是对管道的性能增强。

机制 是否复制内核 存储位置 描述
管道 是(两次复制) 内核缓冲区 基于字节流,需内核复制数据,效率较低,但实现简单
共享内存 内核物理地址 直接访问,无数据复制,效率最高,但需同步机制
消息队列 是(两次复制) 内核物理地址 消息按类型组织,支持异步通信,但需内核复制数据,适合中小数据量传输

进程模型

内核会为每一个进程创建并保存一个名为PCB(Process Control Block)的数据结构,来维护进程运行过程中的一些关键信息。

  1. PID
  2. 进程状态
  3. 进程切换时需要保存和回复的寄存器的值
  4. 内存管理

    当前进程所属哪一块内存,如页表,段表等
  5. 当前工作目录
  6. 进程调度信息

    比如优先级,进程调度指针
  7. I/O状态信息

    最常见的就是文件描述符表(struct fdtable),I/O设备列表
  8. 同步和通讯信息

    比如信号量,信号等。
  9. 权限管理

    比如所属id与所属group
struct task_struct {
/* 进程状态 */
volatile long state; /* 进程标识符 */
pid_t pid; /* 指向父进程的指针 */
struct task_struct __rcu *parent; /* 进程的用户和组信息 */
uid_t uid, euid, suid, fsuid;
gid_t gid, egid, sgid, fsgid; /* 其他成员... */
};

内存模型

内存模型,基本都大同小异,不再赘述.

以C语言程序为例:

栈指针 SP,表示栈顶

帧指针 BP,表示栈底

命令指针 IP,命令指针,指向下一条要运行的命令

进程状态模型

对于进程状态,有一个抽象的定义。

  1. 初始状态(initial)

    进程刚被创建的初始态,该阶段,操作系统会为进程分配资源
  2. 就绪态(ready)

    进程已经准备就绪,当并未被CPU所调度。
  3. 运行态(Running)

    正在CPU上执行代码
  4. 阻塞态(Blocked)

    进程等待其他事件完成,比如网络I/O,磁盘I/O而无法继续时,就处于阻塞状态
  5. 终止态(Final)

    进程执行完毕,准备释放其占用的资源,PCB信息依旧存在。该状态理论上非常短暂。
  6. 僵尸态(Zombie)

    与终止态非常相似,唯一的区别就是因为未知原因PCB长期不释放。

而在Linux中,并未完全遵循上述抽象概念。

不过总体类似,状态主要有如下几种。

  1. D

    不可中断的睡眠状态,比如执行IO操作时,不可中断。
  2. I

    空闲的内核线程
  3. R

    Runnig/Read 运行中或者可以运行的状态
  4. S

    可中断的睡眠状态,比如等待唤醒
  5. T

    由工作控制信号停止
  6. t

    由调试器停止
  7. W

    分页,从2.6内核版本后就不再有效
  8. X

    死亡状态,理论上不会看到,因为相当于整个进程都被回收了,包括PCB,是现实不了的。你如何显示一个不存在的进程?
  9. Z

    僵尸进程,已经终止但PCB尚未被回收
抽象 Linux实现
初始态 N/A
就绪态 R
运行态 R
阻塞态 D,S,T,t
僵尸态 Z

进程状态控制

当一个进程状态变换的时候,通常需要三步。

  1. 找到PCB
  2. 设置PCB状态信息
  3. 将PCB移到响应的队列

    比如进程从阻塞态变成就绪态,状态变化后,CPU的调度队列也要变化。

思考一个问题,如果在第二步的时候,突然来了一个中断,导致第三步没有执行。破坏了原子性,从而使得程序出现异常,这时候应该怎么处理?

汇编每执行一行代码,CPU都会检查一次有没有中断。

这个时候,就要依靠特权指令来实现原子性了

cpu提供两个特权指令

  1. 关中断指令
  2. 开中断指令

因此,在开/关中断指令中间的汇编代码,CPU不会再检查有没有中断,从而实现操作原子性。

Linux学习笔记(三)----进程的更多相关文章

  1. Linux学习笔记(六) 进程管理

    1.进程基础 当输入一个命令时,shell 会同时启动一个进程,这种任务与进程分离的方式是 Linux 系统上重要的概念 每个执行的任务都称为进程,在每个进程启动时,系统都会给它指定一个唯一的 ID, ...

  2. 嵌入式Linux学习笔记(三) 字符型设备驱动--LED的驱动开发

    在成功构建了一个能够运行在开发板平台的系统后,下一步就要正式开始应用的开发(这里前提是有一定的C语言基础,对ARM体系的软/硬件,这部分有疑问可能要参考其它教程),根据需求仔细分解任务,可以发现包含的 ...

  3. Linux学习笔记24——进程管道

    一 管道的作用 通常把一个进程的输出通过管道连接到另一个进程的输入. 二 popen和pclose函数 #include <stdio.h> FILE *popen(const char ...

  4. linux学习笔记之进程

    一.基础知识 1:进程. 1,进程ID: 非负整数,具有唯一性. 1)ID=0的进程:调度进程/交换进程.内核的一部分.不执行任何磁盘上的程序. 2)ID=1的进程:init进程. 1-自举结束时,由 ...

  5. Linux学习笔记(三):系统执行级与执行级的切换

    1.Linux系统与其它的操作系统不同,它设有执行级别.该执行级指定操作系统所处的状态.Linux系统在不论什么时候都执行于某个执行级上,且在不同的执行级上执行的程序和服务都不同,所要完毕的工作和所要 ...

  6. linux学习笔记-13.进程控制

    1.查看用户最近登录情况 lastlastlog 2.查看硬盘使用情况 df 3.查看文件大小 du 4.查看内存使用情况 free 5.查看文件系统 /proc 6.查看日志 ls /var/log ...

  7. linux学习笔记三:防火墙设置

    请注意:centOS7和7之前的版本在防火墙设置上不同,只有正确的设置防火墙才能实现window下访问linux中的web应用. centOS6添加端口: vi /ets/sysconfig/ipta ...

  8. Linux学习笔记<三>

    <1>查看本机的IP地址 命令:ifconfig -a 机器的ip地址是:(inet 地址:172.16.163.57 ) <2>单独查看内存使用情况的命令:free -m 查 ...

  9. linux 学习笔记三

    用户管理篇章 useradd 建立用户 一般用法 #useradd mysql 含义 创建 mysql用户 特殊用户 > #useradd -d /usr/cjh -m cjh 含义 创建 cj ...

  10. 【linux学习笔记三】链接命令

    链接命令:ln link =============华丽的分割线============= ln又有软链接和硬链接 //硬链接特征(不建议创建硬链接) 1.拥有相同的i节点和存储block块,可以看做 ...

随机推荐

  1. springboot之结合mybatis增删改查解析

    1. 场景描述 本节结合springboot2.springmvc.mybatis.swagger2等,搭建一个完整的增删改查项目,希望通过这个基础项目,能帮忙朋友快速上手springboot2项目. ...

  2. 深入理解第三范式(3NF):数据库设计中的重要性与实践

    title: 深入理解第三范式(3NF):数据库设计中的重要性与实践 date: 2025/1/17 updated: 2025/1/17 author: cmdragon excerpt: 在数据库 ...

  3. Python读取txt文本

    转载:Python读取txt文本三种方式 python常用的读取文件函数有三种read().readline().readlines() read() 一次性读取所有文本,在读取文本中含有中文时是gk ...

  4. 部署博客(docker)

    参考:链接 准备一台新的服务器 安全组:8080.80.3306等端口放通 安装docker 参考: 链接 获取最新镜像 docker pull b3log/solo 报错,可能是docker未启动, ...

  5. 基于Linux系统的PXE搭建方法

    本文分享自天翼云开发者社区<基于Linux系统的PXE搭建方法>,作者:t***n 一.底层环境准备 1.安装RedHat7.6系统 2.关闭防火墙和Selinux systemctl s ...

  6. Celery异步分布队列

    Celery分布式任务队列 一.Celery介绍celery periodic task Celery 是一个基于python开发的分布式异步消息任务队列,通过它可以轻松的实现任务的异步处理,如果你的 ...

  7. 洛谷P1983 [NOIP2013 普及组] 车站分级 题解

    思路 由题可知,在一趟车次的区间内,停靠的站点的等级恒大于不停靠的站点. 因此,对于每一趟车次的区间,给所有停靠的站点向所有不停靠的站点两两连有向边,然后求图中最长的路径长度,就能得到答案. 实现 因 ...

  8. 用Logseq记日报和管理文献

    优缺点浅评 Logseq是一款双链笔记软件,其优点结合使用场景概括来说包括 开箱即用的极简界面,非常适合用来写日报 灵活的双链,强大的PDF标注,适合构建文献库 使用markdown格式来本地存储笔记 ...

  9. CentOS7脚本检测SpringBoot项目JAR包变化后自动重启

    #!/bin/bash # 文件目录 fileDir=/usr/local/project/back logDir=/usr/local/project/logs # 设置需要检测的文件路径 file ...

  10. 使用form-create生成表单组件

    FormCreate 是一个可以通过 JSON 生成具有动态渲染.数据收集.验证和提交功能的表单生成组件.支持5个UI框架,并且支持生成任何 Vue 组件.内置20种常用表单组件和自定义组件,再复杂的 ...