3.进程相关

1.关于进程和程序的相关定义

1.1 程序的相关定义

image-20211108111353924

程序通俗来讲就是我们的源代码文件,然后里面还包含了其他的文件信息

程序入口地址:也就是 main 函数的位置

1.2 进程的相关定义

image-20211108112101092

进程需要资源:CPU ,内存

进程是一个抽象定义:它是一系列资源的集合,并不像程序是真真正正存在的

1.3 单道多道程序设计

image-20211108112219464

需要注意:

一个 CPU 一次只能执行一个程序,只不过因为CPU 切换进程时的速度非常快所以我们会以为它执行了多个程序

1.4 时间片

image-20211108112555228

进程调度策略及算法

6 种进程调度算法

  • 先来先服务

  • 最短进程优先

  • 高优先权优先调度算法

    ​ 根据进程在运行过程中是否可以被中断又分为

    • 抢占式
    • 非抢占式
  • 高响应比优先调度算法

  • 时间片轮转法

  • 多级反馈队列调度算法

1.5 并行和并发

image-20211108114808268

1.6 PCB 进程控制块

image-20211108114924962 image-20211108115315030

进程控制块又称进程描述符,可以和文件描述符结合起来

进程控制块记录的是进程的信息,他就是一个 struct 

1.6.1 如何查看 Linux 上限资源的信息

1.查看 Linux 上限资源信息

ulimit -a
image-20211108115609031

2.修改 Linux 上限资源

修改可以运行的进程资源数为 :100

ulimit -u 100 // 括号里面的参数+修改的值

1.7.进程的状态

进程分为三种状态和五种状态两个分类

三种状态分为:就绪态,阻塞态,还有运行态。五种状态分为新建态,就绪态,运行态,阻塞态和终止态。

1.三种状态及三种状态的转换过程

image-20211130102246128

就绪态(->运行):进程具备运行条件,等待系统按照一定的分配方式分配到 CPU 后就可以执行,由就绪态变为执行态。处在就绪状态的进程可能有多个,他们会形成一个队列,那个队列叫就绪队列。

阻塞态(->就绪):又称为等待 wait 或者睡眠 sleep 状态,这时进程正等待某件事情的发生,如输入输出的完成。当资源得到满足后或者数据处理完毕就会进入到就绪状态。

执行态:进程占用 CPU 在运行。

执行->阻塞:正在执⾏的进程因发⽣某等待事件⽽⽆法执⾏,则进程由执⾏状态变为阻塞状态。比如说:输入输出请求;申请主存或者外部存储设备资源;出现了一些故障或者读写错误之类的。。。

执行->就绪:正在执行的程序,因为时间片用完而暂停运行,或者在抢先式优先级调度算法系统中优先级更高的进行将其中断了。

2.五种状态

image-20211108120739534

2.进程的相关指令

image-20211108122025589

直接使用 man 可以查看指令的信息

man ps 

2.1 aux 显示进程的信息

1.查看进程信息

ps -aux
tty // 查看当前正在执行的指令

STAT:进程状态

START:进程开始的时间

TIME:进程持续的时间

COMMAND:执行的哪个命令产生的这个进程

image-20211108122520583

image-20211108122656751

2.进程的相关状态

image-20211108122750212

2.2 ajx 列出与作业控制相关的信息

ps ajx

PPID:父进程 ID

PID:进程 ID

PGID:进程可以分组,组 ID

SID:会话 Session ID ,将组再进行整合

可以将组 ID 和会话 ID 想象成学校学院班级之间的关系

image-20211108123246062

2.3 top 实时显示进程动态

top

直接在实时的窗口中输入以下指令,使用 q 退出实时显示

image-20211108123515572 image-20211108123853199

2.4 kill 杀死进程

kill 进程号
image-20211108123958673

2.4.1 kill -l 查看 kill 的信号

像 9 就是 kill 的一个信号,这个信号是一个宏代表一个操作。有时候发现 kill 不掉一些进程就可以使用信号进行相关操作

kill -l
image-20211108124351958

E.g.

bash 指令是当前控制台的控制程序(对于控制台的操作可以看做是一个进程),系统为了安全起见,使用 kill 2930 指令关闭控制台是无效的。所以就要使用 kill -9 2930 将控制台进程暂停掉

kill -9 2930
kill -SIGKILL 2930 // 直接使用宏也是可以的

image-20211108124451307

2.5 pid_t 进程号变量

关于进程号的相关函数

image-20211108125123365

getpid():得到当前进程编号

getppid():得到父亲进程编号

getpgid():得到组进程编号

3.进程创建:fork

3.1 fork 在父进程中创建子进程

在 lesson18 fork.c 中进行记录

3.1.1 API

image-20211109104740303

当父进程访问这个程序时返回的 id 是 >0 的,当子进程调用这个程序时返回的 id 是 0

3.1.2 代码

下面实现了书写一个程序,在程序中打开一个进程去执行程序中的 for 循环代码

#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
using namespace std;
int main(){
  // 首先创建一个程序,在程序中创建进程,创建的进程会执行这个程序
  pid_t pid = fork(); // 创建一个新的进程
  // 判断当前是父进程还是子进程
  if(pid>1){
    printf("pid:%d\n",pid);
    printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());

  }
  if(pid==0){
    printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
  }
  // 创建 for 循环操作
  for(int i =0;i<100;i++){
    printf("i:%d,pid:%d\n",i,getpid());
  }
  return 0;
}

3.1.3 代码演示

从结果中可以看出,执行这个程序时需要一个进程,这个是系统帮我们开出的,然后在此进程

的基础上人为又开了个进程,这两个进程一同执行程序

** fork 调用时会有两次返回值,一次是在父进程,一次在子进程**

image-20211109112410402

image-20211109112819914

其中 3155 进程就是命令窗口进程

子进程和父进程都会执行这个程序:

这是因为父进程和子进程调用的代码是不一样的。因为父进程和子进程 id 是限制住的,导致执行的代码不一样

image-20211109113231539

3.1.4 子进程和父进程的读时共享写时拷贝(copy-on-write)

子进程是对父进程的复制,内存空间也是直接复制

image-20211109113655523

内存空间的使用情况:

因为是直接复制的虚拟空间,所以两个进程的虚拟空间中数据都是一样的,唯一不一一样的就是保存在栈空间的 pid

我们的程序会根据栈空间的值去判断应该执行哪一段程序

image-20211109113913234

写时拷贝:

写时拷贝参考资料

image-20211109115326053image-20211109115340816

内核此时并不复制整个父进程的空间,而是让父子进程共享同一个地址空间,指针指向同一个变量地址
只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间
	如果只对某个变量进行读取:直接读
	写:假设更改 num = 10 的数据。本来都是指向物理空间的 num10 ,当写的时候子进程开辟一个新空间向物理空间写数据

4.进程创建:GDB 实现多进程调试

在 lesson18 hello.c 中进行记录

4.1 代码

这里先创建一个进程,不同的进程会执行不同的函数。

其中父进程打印的是 i ,子进程打印的是 j

#include <stdio.h>
#include <unistd.h>
int main() {
    printf("begin\n");
    if(fork() > 0) {
        printf("我是父进程:pid = %d, ppid = %d\n", getpid(), getppid()); // line 10
        int i;
        for(i = 0; i < 10; i++) {
            printf("i = %d\n", i);
            sleep(1);
        }

    } else {
        printf("我是子进程:pid = %d, ppid = %d\n", getpid(), getppid()); // line
        int j;
        for(j = 0; j < 10; j++) {
            printf("j = %d\n", j);
            sleep(1);
        }
    }
    return 0;
}    

4.2使用 GDB 进行调试

image-20211109130916819

4.1 GDB 调试父进程

1.添加 GDB 调试

gcc hello.c -o hello -g // 生成 GDB 编译
gdb hello // 使用 GDB 的方式打开软件

2.添加断点

在第 10 行和第 20 行添加断点

b 10 // printf("我是父进程:pid = %d, ppid = %d\n", getpid(), getppid()); 
b 20 // printf("我是子进程:pid = %d, ppid = %d\n", getpid(), getppid()); 
i b // 查看断点情况
r // 执行程序
image-20211109124145827

下面使用 r 执行代码:

可以看到在执行到父进程的指令时程序停止了,接着执行子进程的程序,直到子进程执行完毕,但是发现,这时候 gdb 还能输入东西

image-20211109125808387
c // 继续执行代码

从下图发现父进程则继续执行

这里对子进程设置的断点没有执行,原因是 GDB 默认调试父进程的程序,不调试子进程产生的断点

image-20211109124923046

4.2 GDB 如何调试子进程

4.2.1 show follow-fork-mode 查看当前 GDB 调试的进程

show folllow-fork-mode

image-20211109125239226

4.2.2 show/set follow-fork-mode [parent(默认)|child] 设置 GDB 跟踪父进程还是子进程

在原有进程上调整执行的进程

set follow-fork-mode child
r // 重新运行程序
image-20211109130637857
n // 进行下一行指令操作

从下图可以看到后面再调试的都是子进程

image-20211109130736578

4.3 show/set detach-on-fork[on|off] 设置调试模式

像上面的程序,当父进行执行到断点的时候子进程还是一直执行的。这个函数可以设置另一个进程不执行

on-调试进程执行时其他进程继续执行

off-调试进程执行时其他进程不执行

show detach-on-fork // 查看调试模式
set detach-on-fork [on|off] // 设置调试模式
image-20211109132313526

4.4 查看,切换调试的进程

info inferiors // 先查看当前执行的进程
inferior +number // 然后切换执行的进程
image-20211109132748393

*指向的地方就是当前执行的继承,当时用进程切换时就可以切换到子进程,这个时候调试得就是子进程的代码

当进入到子进程的程序段后别忘了在写一个 c 程序才会继续执行

c
image-20211109132847485

进程执行完毕

5632 进程已经执行完毕,默认切换成子进程,然后继续再调试子进程

image-20211109133532651
detach inferiors +id // 跳出这个进程的 debug

脱离了 debug 之后程序就会继续执行

image-20211109134203571

5.进程创建:exec 函数族

5.1 函数族函数介绍

image-20211110103934871

5.2 虚拟空间变化

image-20211109225546843 image-20211109225833873

正是因为原函数的虚拟内存会被销毁,所以需要创建一个子进程用于执行调用函数

并且最后子进程不会再返回到父进程当中

5.3.相关调用函数

5.3.1 execl 执行可执行程序和Shell指令

1.执行自定义的某个程序

1.API

image-20211109225915052

这里传入的是文件的执行文件,而不是 C 文件

调用失败后:内核还是继续执行调用函数,调用函数最后是有返回值的。调用成功后执行的是 a.out ,这个是没有设置相应的返回值的

2.代码

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork(); // 创建子进程
    if(pid>0){ // 执行父进程
        printf("i am parent:%d",getpid());
        sleep(1);
    }

    else if(pid==0){ // 执行子进程
        execl("hello","hello",NULL); // 第一个参数一般写可执行程序的名称,最后以 NULL 结尾
        printf("i am child im back");// 这句话不会再输出
    }
    for(int i =0;i<3;i++){ //
        printf("i=%d,pid=%d\n",i,getpid());
    }

    return 0;
}

3.代码演示

image-20211110100140290

需要注意1: 当子进程调用执行文件之后不会再返回到原有调用它的进程当中

可以看到 “i am child im back” 这句话并没有输出,说明这个进程就去执行执行程序不再返回去了

后面再执行 for 循环方法的是父进程

2.执行 shell 指令

1.如何查看系统程序保存在哪

刚才的 execl 需要传入可执行文件的路径,使用以下命令行就可以看到某个执行文件的保存地址,以 ps 指令为例

which +指令(执行文件)
E.g. :which ps

2.执行 Shell 指令

execl("/bin/ps","ps","aux",NULL); // aux 是传递的参数

可以看出执行这个文件时就相当于执行了 ps -aux

image-20211110101527844

5.3.2 execlp 执行可执行文件

1.API

image-20211110102646471

5.4函数族的调用函数命名规则

execlp 结尾是 p ;execl 结尾是 l

p(path) :按 PATH 环境变量指定的目录搜索可执行文件

所以定义 execlp 时如果不传入文件的绝对路径依旧可以通过环境变量找到该文件

但是 execl 没有 p ,所以如果调用 execl 方法不传入具体的绝对路径或者可执行文件没有和 .c 文件在一个目录下,则这个可执行文件就找不到,并且不会被执行

l(list):参数地址列表,以空指针结尾

可以看到两个方法都有 l ,并且我们传入的参数是一个字符串(参数地址列表),以NULL 结尾

下图中 execv 使用的是 v 出入的就是参数数组

image-20211110103737103

6.进程控制

6.1 进程退出

6.1.1 exit 退出进程

image-20211110105046610

C 的 exit() 标准库函数会进行函数处理操作和 IO 刷新缓冲区的操作

Linux 则不会

1.API

image-20211110111051469

2.代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
        printf("Hello\n");
        printf("world");
        _exit(0); // Linux
        //exit(0); // C
        return 0;
}

3.代码演示

(1) Linux 中的输出

可以看出没有输出 world ,这是因为 C 会将字符串保存在缓冲区中,而 \n 具有对缓冲区的刷新操作,所以Hello 会打印出来,但是 _exit() 是没有刷新缓冲区的操作的,所以这里的 world 还是保存在缓冲区内

image-20211110110252266

(2)C 标准库的输出

这里输出的 world 是因为 C 的 exit 会对缓冲区内的字符串进行刷新操作

image-20211110110901388

6.2进程相关定义

6.2.1 孤儿进程

1.解释说明

image-20211110111629453

父进程已经运行完,但是子进程还没运行完,父进程没有办法回收子进程资源,init 进程就会回收孤儿进程 ,init 的 ppid 为 1。孤儿进程没有什么危害

2.代码

#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main(){
        // 首先创建一个程序,在程序中创建进程,创建的进程会执行这个程序
    pid_t pid = fork(); // 创建一个新的进程
        // 判断当前是父进程还是子进程
    if(pid>1){
        printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());

    }
    if(pid==0){
         sleep(1);
        printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
    }
    // 创建 for 循环操作
    for(int i =0;i<3;i++){
        printf("i:%d,pid:%d\n",i,getpid());
    }
        return 0;
}

3.代码演示

image-20211110112741943

①:父进程先执行,看到其 id 和基于 bash 的 ppid

②父进程已经执行完毕,这时候父进程被其父进程 bash 回收,所以在这里出现了控制台的指令

③子进程开始执行,但是本应该调用他的父进程已经被销毁,所有由 init 去执行和销毁它,ppid 是1

6.2.2 僵尸进程

代码保地址:/home/xu/C++/lesson20 /zombie.c

1.定义

子进程的 PCB 需要由父进程释放,一个进程内核区的 PCB 没有被释放掉,残留的资源一直存放在内存中,并且不能用 kill -9 杀死。但是系统所能使用的进程号有限,导致没有办法创建新的进程,僵尸进程是有害的进程。

2.代码

#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main(){
    // 首先创建一个程序,在程序中创建进程,创建的进程会执行这个程序
    pid_t pid = fork(); // 创建一个新的进程
    // 判断当前是父进程还是子进程
    if(pid>1){
        printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
        while(1){
            sleep(1);
            printf("i am zombie\n");
        }

    }
    if(pid==0){
        printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
    }
    return 0;
}

3.代码演示

image-20211110115602351

子进程已经执行完 < defunct >,父进程还在执行,所以父进程没有办法释放子进程的空间,那么子进程就变成了僵尸进程

6.2.3 进程回收-wait 函数使用

image-20211110120907998

进程退出时内核回收资源,主要是 PCB 中的信息

1.API

image-20211110121358927

返回值设置:

子进程状态改变:返回状态改变的子进程 pid

子进程状态没有改变 return 0

失败:返回 -1

此函数的阻塞行为:

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)

如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

2.代码

1.不获得子进程被杀死时的状态

首先先创建 5 个子进程

父进程进入无限循环状态:首先打印一个 id ,然后使用 wait 函数将父进程阻塞

子进程也要执行一段无限循环的代码,但是在父进程在阻塞的情况下可以用 kill -9 将子进程杀死

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
    //创建 5 个子进程
    pid_t pid;
    for(int i=0;i<5;i++){
        pid=fork();// 为了避免孙子进程,使用相同的pid 创建子进程
        if(pid==0) break; // 子进程不进行任何操作
    }
    if(pid>0){
        // 父进程一直执行
        while(1){
            printf("parent:%d\n",getpid());
            // 父进程在打印一句话之后进入阻塞状态
            int ret = wait(NULL); // 传入 NULL 则不处理任何状态
            if(ret==-1){ // 父进程阻塞失败或者没有子进程可以杀了,返回 -1
                printf("all child die,pid=%d\n",ret);
                break;
            }
            // 所有的子进程全部处理完毕,打印
        }
    }
    else if(pid==0){
        while(1){
            // 子进程操作
            printf("child:%d\n",getpid());
            sleep(1);
        }
    }
    return 0;
}
image-20211110130444851

2.获得子进程被杀死时的状态

image-20211110132040456

父进程相关判断代码:

int st; // 定义一个赋值参数,函数可以向这个参数赋值
int ret = wait(&st); // 让父进程等进程结束

if(ret == -1) { 
    break;
}
// 判断子进程杀死的状态
if(WIFEXITED(st)) {
    // 是不是正常退出
    printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
    // 是不是异常终止
    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

子进程相关退出代码:

exit(0); // 在子进程相应的代码块中调用 exit ,在 exit 中传入相应参数则父进程的 wait 函数就会接收到
kill -9 pid // 下方显示的效果

image-20211110134016646

4.知识扩展–如何创建子进程

pid_t pid;
for(int i=0;i<5;i++){
    pid=fork();// 为了避免孙子进程,使用相同的pid 创建子进程
    if(pid==0) break; // 子进程不进行任何操作
}

如何防止创建孙子进程。因为子进程也是需要执行程序文件的。如果不断的使用 fork()去调用子进程执行到这个方法还会在创建子进程的子进程,所以要想创建多个子进程就要用同一个 fork 方法

虽然使用的都是相同的 pid ,但是子进程的 id 却不同

image-20211110124015900

6.2.4 进程回收-waitpid 方法使用

wait() 方法在进程回收时只能阻塞父进程,waitpid 方法回收进程时有更多的选项

**1.API **

image-20211111103215881

2.代码

int st;
int ret = waitpid(0,&st,0);// 监控进程变化

完整代码

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
    pid_t pid;
    // 创建 5 个子进程
    for(int i=0;i<5;i++){
        pid = fork();
    }
    // 判断当前进程
    if(pid>0){
        while(1){
            printf("i am parent:%d",getpid());
            // 监控进程
            int st;
            int ret = waitpid(0,&st,0);// 监控进程变化
            if(ret==-1){
                break;
            }
            else if(ret==0){ //说明还有子进程没有改变状态
                continue;
            }
            else if(ret>0){ // 如果子进程状态发生了变化
                if(WIFEXITED(st)){
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }
                printf("child die, pid = %d\n", ret);
            }
        }
    }
    else{
        while(1){
            printf("child pid%d\n",getpid());
            sleep(1);
        }
        exit(0);
    }
    return 0;
}

3.代码演示

代码演示像上面一样

面试:7.进程通信 IPC

7.1 相关概念

1.什么是 IPC

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的⽅式通常有管道

(包括⽆名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams ⽀

持不同主机上的两个进程 IPC。

Linux 中如何进行进程通信

image-20211111114433675

7.2 管道

7.2.1 管道定义

image-20211111115635646

匿名管道就是我们俗称的管道。ls 可以理解为一个文件,这个文件实现的是将当前目录下的所有文件名显示到终端。但是管道有一个重定向作用,它不将文件名显示在终端而是显示在另一个进程文件 wc 中

管道就是在内存中的缓冲器

7.2.2 管道的特点

image-20211111121048870

1.管道是一个保存在内存中的缓冲区,保存在内核空间

2.管道和有名管道的区别

3.管道是一个字节流

4.管道读写的数据是顺序的,数据结构是队列

5.管道是半双工,具有固定的读端和写端

单工:遥控器只能向电视发送信号,但是电视不能向遥控器发送信号

半双工:类似对讲机,虽然两个人可以相互发消息,但是有一方发消息的时候另一方就要等着

双攻:二者同时相互的发送消息

6.管道数据被读走就永远被抛弃,管道的数据结构是循环队列

当在队列中前面的数据产生删除后,队列还是可以被利用

image-20211111122306527

7.匿名管道只能在有关系的进程间通信

比如说父子进程,他们使用的是同一个文件描述符表,所以父进程的文件描述符 fd 对应的管道也和子进程文件描述符对应的管道是一样的

但是如果是无关的进程,虚拟空间不存在同一指向问题,所以相同的文件描述符 fd 对应的可能是不同的管道

7.3管道的相关函数

7.3.1 pipe–创建匿名管道

1.API

image-20211111130834690

一定要在 fork 之前创建管道,否则进程已经创建完了管道不一定对应的是同一个管道。

pipe 返回的是文件描述符,一个对应管道读端,一个对应管道写端

管道默认是阻塞状态,也就是说子进程如果向父进程发送数据,但是管道中没有数据, read 阻塞,如果管道满了 write 阻塞

2.代码

实现子进程向父进程发送数据

int ret = pipe(pipefd); // 创建管道,得到管道输入端和输出端的文件描述符
write(pipefd[1],str,strlen(str)); // 先向管道中写入数据
read(pipefd[0],buf,sizeof(buf)); // 从管道中读取数据
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include<string.h>
int main(){
    // 定义传出参数
    int pipefd[2];
    // 在创建子进程之前创建管道
    int ret = pipe(pipefd);
    if(ret==-1) perror("pipe");
    // 创建子进程
    pid_t  pid;
    pid = fork(); 
    if(pid>0){ // 父进程读取数据
        char buf[1024]={0};
        int len = read(pipefd[0],buf,sizeof(buf)); // 传入读取端的描述符
        printf("parent recv:%s,pid:%d\n",buf,getpid());
    }
    if(pid==0){// 子进程写入数据到管道
        char* str = "hello world";
        write(pipefd[1],str,strlen(str)); // 传入写入端的描述符
    }
    return 0;
}

PS: 视频末尾还有一个 while 循环的事实消息发送我没有实现

3.代码演示

从下面就可以看到父进程收到了子进程发来的消息

image-20211111132828255

4.管道之间的相互通信

7.3.2 alimit Linux 指令查看管道大小

ulimit -a
image-20211111135201280

每个管道中有 8 块,每一块是 512 的大小,4K

更改管道大小

ulimit -p

7.3.3 fpathconf–查看管道大小

关键代码

long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size : %ld\n", size);
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {

    int pipefd[2];

    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);

    printf("pipe size : %ld\n", size);

    return 0;
}

执行文件输出管道大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CTYKR2ic-1644492619031)(/Users/xuguagua/Documents/typora_image/image-20211111135454268.png)]

7.3.4 C 实现 Linux 管道通讯指令

代码在 lesson22 parent_child.c

image-20211112105848647

其实在 Linux 中指令 ps aux | grep xxx 操作就是实现 ps 进程和 grep 进程之间的通讯

1.代码

Step1:

创建一个管道:用于两个进程通信

Step2:

创建子进程,对父进程和子进程判断

Step3:

子进程:

​ 先将 ps 原先要输出的地方进行重定向,将控制台的输入输出输入到管道

​ 执行 ps 指令

父进程:

​ 读取管道中的数据

int main(){
    // 0.首先创建管道
    int fd[2];
    pipe(fd);
    // 1.创建子进程
    pid_t pid = fork();
    // 2.判断进程
    if(pid>0){ // 进行读操作
        // 关闭写端
        close(fd[1]);

        // 从管道中循环的取数据
        int len =-1;
        char buf[1024]={0};
        while((len=read(fd[0],buf,sizeof(buf)-1))>0){
            // 数据的处理操作
            printf("%s",buf);
            memset(buf,0,1024); // 清空 buf 中的值
        }
        wait(NULL); // 阻塞自己等待子进程执行完毕
    }
    else if(pid==0){ // 写操作
        // 关闭读端
        close(fd[0]);
        // 文件描述符重定向:将数据在控制台打印
        dup2(fd[1],STDOUT_FILENO);
        // 执行 ps aux
        execlp("ps","ps","aux",NULL);
        exit(0);
    }
    return 0;
}

2.代码演示

可以看到读取到了相关的进程数据

image-20211112114626313

3.需要注意

某一个进程不可以同时进行读操作或者写操作,所以当一个进程进行读操作时就要将写操作进行关闭

close(fd[1]); // 关闭写操作

7.4 管道的读写特点

读管道:

​ 管道中有数据:read 返回实际读到的字节数。==》 这是正常情况

管道中无数据:

​ 写端被完全关闭,read 返回 0 ,相当于读到文件末尾

​ 写端没有完全关闭:read 以为写端还要写入数据,read 阻塞等待

写管道:

​ 读端全部关闭:写管道本想写,但是没有读端,进程异常终止 (进程收到 SIGPIPE信号)

​ 管道读端没有全部关闭:

​ **管道以满:**这时候不能再向管道中写入数据,write 阻塞

​ **管道没有满:**write 将数据写入,并返回实际写入的字节数 ==》 这是正常情况

7.4.1 设置管道非阻塞

管道就是文件描述符,如果想要设置管道非阻塞就要设置文件描述符为 open 状态

更改文件状态 fcntl

1.API

image-20211112121659547

2.读操作阻塞时的代码

在子进程中每发送完一段数据 write 端就进行阻塞,即 sleep 操作,写操作是没有被完全关闭的,这时候读操作阻塞

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
*/
int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);

        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        close(pipefd[0]);
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }

    }
    return 0;
}

3.代码演示

image-20211112122936052

5.更改文件描述符属性,让 read 不阻塞

关键代码:

需要在父进程中添加对读指令的 open 操作,使用 fcntl 指令

int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
flags |= O_NONBLOCK;            // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag

父进程的进程操作如下:

int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
flags |= O_NONBLOCK;            // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag
while(1) {
    int len = read(pipefd[0], buf, sizeof(buf));
    printf("len : %d\n", len);
    printf("parent recv : %s, pid : %d\n", buf, getpid());
    memset(buf, 0, 1024);
    sleep(1);
}

6.设置不阻塞后代码演示

和上面代码相比这里 read 不阻塞后会一直打印数据,只不过这时候管道中没有数据,所以打印的数据就是 -1

image-20211112124138013

7.3 有名管道

(1)FIFO 可以在⽆关的进程之间交换数据,与⽆名管道不同;

(2)FIFO 有路径名与之相关联,它以⼀种特殊设备⽂件形式存在于⽂件系统中。

有名管道可以进行非“关系” 间的节点通信,就定义了一个文件,两个进程通讯时就是对那个文件进行读写操作

数据结构:关系队列

与普通文件的区别是,有名管道生成的文件在读出之后就不存在了

7.3.1 使用 Linux 命令行创建有名管道 fifo

1.指令

mkfifo fifo // 定义一个名字叫 fifo 的有名管道

2.命令演示

创建完 fifo 之后,在所在目录下生成了一个 fifo 的文件

image-20211112134756724

这里向 fifo 这个文件写入数据 hello world 。写入完数据后进程阻塞,是因为这个 fifo 在等待读取

image-20211112134651875

7.3.2 mkfifo 函数创建有名管道

代码在lesson23

1.API

image-20211112134935892

这里赋予的文件权限类似于文件的文件权限

2.代码:实现两个进程间的通信

①write 文件,创建有名管道,并向管道中写数据

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include<string.h>
int main(){
	// 创建管道之前先判断管道是否存在
	int ret = access("test",F_OK); // 判断一个文件是否存在
	if(ret==-1){ // 管道不存在返回 -1 
		printf("管道不存在正在创建中");
		mkfifo("test",0664);
	}
	// open 管道文件的文件描述符
	int fd = open("test",O_WRONLY);
	for(int i =0 ;i<100;i++){
		char buf[1024];
		sprintf(buf,"hello,%d\n",i);
		printf("write data:%s\n",buf);
		write(fd,buf,strlen(buf));
		sleep(1);
	}
	close(fd);
	return 0;
}

②read,创建读操作,将数据读出

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
	// 打开管道
	int fd = open("test",O_RDONLY);
	char buf[1024] = {0};
	while(1){
		int len = read(fd,buf,sizeof(buf));
		if(len==0){
			printf("写端断开连接");
			break;
		}
		printf("recv buf:%s\n",buf);
		memset(buf,0,1024);
	}
	close(fd);
	return 0;
}

3.代码演示

首先先执行 write.c 的文件,但是因为 read 还没有打开,所以这时候的write是阻塞状态

image-20211112152223190

然后执行 read 文件,read 文件就接着可以显示读到的数据

image-20211112152710312

与此同时 write 控制台也开始写文件

image-20211112152836247

当关掉其中一个时另一个也就不读或者不写了,这是因为发生了读、写操作的阻塞

image-20211112152912272

7.4 聊天功能实现–有名管道实战

代码详见 lesson24

7.4.1 整体流程

这里需要创建两个管道,因为一个进程只能控制管道的一端,如果要实现双向通信需要两个管道。

image-20211113095304185

7.4.2 代码

导入相关库文件

#include <sys/types.h>
#include<stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include<stdlib.h>
#include<string.h>

1.A 端代码

①创建有名管道

创建有名管道时判断有名管道文件是否存在

// 创建有名管道1
    int ret = access("fifo1",F_OK);
    if(ret==-1){
        printf("管道 1 创建中。。。\n");
        ret = mkfifo("fifo1",0664); // A 对于管道1 使用只写的方式打开
        if(ret==-1){
            perror("mkfifo");
            exit(0);
        }else{
            printf("打开管道1成功。。。\n");
        }
    }
    // 创建有名管道2
    ret = access("fifo2",F_OK);
    if(ret==-1){
        printf("管道 2 创建中。。。\n");
        ret = mkfifo("fifo2",0664); // A 对于管道1 使用只写的方式打开
        if(ret==-1){
            perror("mkfifo");
            exit(0);
        }else{
            printf("打开管道2成功。。。\n");
        }
    }

②分别获得两个管道的写入端和读取端

// 对于两个管道的操作
int fd1 = open("fifo1",O_WRONLY);
int fd2 = open("fifo2",O_RDONLY);

③ while 循环监听两端读取操作

char buf[128];
while(1){ // A 的监听操作
    memset(buf,0,128);
    fgets(buf,sizeof(buf),stdin); // 最后一个参数是从哪里获得这里是从控制台的标准输入输出获得
    write(fd1,buf,sizeof(buf));
    memset(buf,0,128);
    // 定义读操作
    ret = read(fd2,buf,sizeof(buf));
    printf("A recv:%s\n",buf);
}

④关闭两端

// 关闭两个管道
close(fd1);
close(fd2);

2.B 端关键代码

①获得两个管道的通配符

// 对于两个管道的操作
int fd1 = open("fifo1",O_RDONLY);
int fd2 = open("fifo2",O_WRONLY);

② while 循环监听输入输出操作

char buf[128];
while(1){ // B 的监听操作
    memset(buf,0,128);
    // 定义读操作
    ret = read(fd1,buf,sizeof(buf));
    printf("B recv:%s\n",buf);
    memset(buf,0,128);
    fgets(buf,sizeof(buf),stdin); // 最后一个参数是从哪里获得这里是从控制台的标准输入输出获得
    write(fd2,buf,sizeof(buf));
}

7.4.3 代码演示

下图所示,AB 两端的文件都可以收到

现在的 Bug 就是读写都在一个进程中:

有可能发生阻塞的情况。

比如 A 代码中先定义写操作,这个时候 B 向 A 发送消息 A 是读不到的

但是 B 先定义的是读操作,如果 A 先给 B 发送消息 ,B 可以正常读到

image-20211113111215469
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值