Linux学习笔记(三)—-进程

简介

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

进程树

Linux系统通过父子进程关系串联起来,所有进程之前构成了一个多叉树结构。
Linux学习笔记(三)----进程

孤儿进程

孤儿进程是指父进程已经结束,子进程还在执行的进程。那么此时此刻,该进程就变成了孤儿进程。
当进程变成孤儿进程后,系统会认领该进程,并为他再分配一个父进程(就近原则,爸爸的爸爸,爸爸的爸爸的爸爸)。

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

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

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

管道(pipe)

Linux学习笔记(三)----进程

匿名管道

半双工通信,即数据只能在一个方向上流动。只能在具有父子关系的进程之间使用。
其原理为:内核在内存中创建一个缓冲区,写入端将数据写入缓冲区,读取端从缓冲区中读取数据

点击查看代码
#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: %sn", 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: %sn", buffer);     close(fd);      // 删除命名管道 	// 理论上,命名管道是可以重复使用的,其本质就是操作文件而已。只是不建议这么操作。     unlink(FIFO_NAME);      return 0; } 

共享内存(Shared Memory)

多个进程共享同一块内存地址,是最快的IPC方式。
其原理为:内核在物理内存中分配一块内存区域,多个进程将内存地址映射到自己的虚拟空间内,从而可以直接读写该区域。
Linux学习笔记(三)----进程

点击查看代码
#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; } 

代码执行中:生成的临时文件
Linux学习笔记(三)----进程

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

消息队列(message queue)

消息队列是消息的链表,存放在内核中,由消息队列标识符标识。进程可以向队列中添加消息,也可以从队列中读取消息。
其原理为:内核为每个消息队列维护一个消息链表,消息可以按照不同的类型进行分类,进程可以根据消息类型有选择地读取消息。

Linux学习笔记(三)----进程

生产者
#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("接受到来自生产者的信息%sn",read_buff);         }     }     //关闭资源     close(mqdes);     unlink(mq_unlink);     return 0; }  

临时文件

与内存共享类似,Linux底层也会生成一个临时文件来代表队列。
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语言程序为例:
Linux学习笔记(三)----进程

栈指针 SP,表示栈顶
帧指针 BP,表示栈底
命令指针 IP,命令指针,指向下一条要运行的命令

进程状态模型

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

  1. 初始状态(initial)
    进程刚被创建的初始态,该阶段,操作系统会为进程分配资源
  2. 就绪态(ready)
    进程已经准备就绪,当并未被CPU所调度。
  3. 运行态(Running)
    正在CPU上执行代码
  4. 阻塞态(Blocked)
    进程等待其他事件完成,比如网络I/O,磁盘I/O而无法继续时,就处于阻塞状态
  5. 终止态(Final)
    进程执行完毕,准备释放其占用的资源,PCB信息依旧存在。该状态理论上非常短暂。
  6. 僵尸态(Zombie)
    与终止态非常相似,唯一的区别就是因为未知原因PCB长期不释放。

Linux学习笔记(三)----进程

而在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不会再检查有没有中断,从而实现操作原子性。

发表评论

评论已关闭。

相关文章