简介

进程是系统资源分配的最小单位,它曾经也是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. Java虚拟机调优-典型配置举例

    背景: 以下配置主要针对分代垃圾回收算法而言. 堆大小设置 年轻代的设置很关键 JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制:系统的可用虚拟内存限制:系统的 ...

  2. ResponseBody与RequestBody-copy

    1. ResponseBody @ResponseBody的作用其实是将java对象转为json格式的数据. @responseBody注解的作用是将controller的方法返回的对象通过适当的转换 ...

  3. Java技术栈总结-基础

    - - -计算机技术演化- - -1 编程语言演化1.1 写在最前  此文用于个人总结,串接知识点 1.2 汇编  举例:mov .add  特点:程序量很大,几百行.几千行乃至几万行 1.3 VB- ...

  4. Linux curl brew命令详解

    命令:curl 在Linux中curl是一个利用URL规则在命令行下工作的文件传输工具,可以说是一款很强大的http命令行工具.它支持文件的上传和下载,是综合传输工具,但按传统,习惯称url为下载工具 ...

  5. SqlServer中根据某几列获取重复的数据将其删除并保留最新一条

    有时候,我们某个数据表中,可能有几列的数据都是一样的,此时我们可能想查询出这几列数据相同的所有数据行,并保留最新一条,将其他重复的数据删除. 1.ROW_NUMBER函数 假设我们有如下数据表: 此时 ...

  6. macos修改hosts后清理dns缓存

    sudo killall -HUP mDNSResponder sudo killall mDNSResponderHelper sudo dscacheutil -flushcache

  7. 从 14 秒到 1 秒:MySQL DDL 性能优化实战

    1. 问题背景 MySQL版本:8.0.30 测试表数据量:200万 在 MySQL 中,研发人员最初执行了以下 SQL 语句,向表 t_email 中添加了一个允许为 NULL 的列 id3,并设置 ...

  8. CF935D Fafa and Ancient Alphabet 题解

    讲一个很暴力的方法(为描述方便,下文 \(a\) 数组代表 \(s1\),\(b\) 数组代表 \(s2\)). 发现假如当前 \(a_i\ne b_i\),就不需要再向下枚举了,于是拥有了分类讨论的 ...

  9. autMan奥特曼机器人-内置Redis

    autMan内置了redis服务,有的脚本运行需要redis支持 几个注意事项: 启用redis服务后要重启autMan生效,关闭一样的道理. 启用redis服务后会增加约200M的内存占用 多个au ...

  10. Postman 接口测试工具详解

    一.引言 在软件开发和测试过程中,接口测试是至关重要的环节.Postman 作为一款功能强大的接口测试工具,为开发者和测试人员提供了便捷高效的测试解决方案. 二.Postman 简介 Postman ...