c++高级篇(一) —— 初识Linux下的进程控制

linux的信号

信号的概念

在Linux中,信号是一种用于进程间通信和处理异步事件的机制,用于进程之间相互传递消息和通知进程发生了事件,但是,它不能给进程传递任何数据。

信号产生的原因有很多种,在shell中,我们可以使用killkillall来发送信号

kill -信号的类型  进程编号
killall -信号的类型 进程名

信号的类型

常见信号类型:

  • SIGINT:终止进程(键盘快捷键 ctrl+c
  • SIGKILL: 采用kill -9 进程编号,强制杀死程序

信号的处理

进程对信号的处理方法一般有三种:

  • 对该信号进行默认处理,一般是终止该进程
  • 设置中断的处理函数,受到该型号的函数进行处理
  • 忽略该信号,不做如何处理

signal()函数可以设置程序对信号的处理方式

函数的声明:

sighandler_t signal(int signum,sighandler_t handler)

注释

参数signum表示信号的编号

参数handler表示信号的处理方式,有三种情况:

  1. SIG_DFL:恢复参数signum所指信号的处理方法为默认值
  2. 一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数
  3. SIG_IGN:忽略signum所指的信号

示例代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void func(int signum)
{
    cout<<"收到了信号"<<signum<<endl;
    signal(1,SIG_DFL);//将函数的处理方式由自定义函数改为了默认方式处理
}

int main(int argc,char *argv[],char *envp[])
{
    signal(1,func);
    signal(15,func);
    signal(2,SIG_IGN);//忽略信号2
    while(1)
    {
        cout<<argc<<endl;
        sleep(1);
    }
    return 0;
}

信号的作用

​ 服务程序运行在后台,如果想终止它,一般不会直接杀死它,以防止出现意外。

​ 我们·一般会选择向进程去发送一个信号,当程序收到这个信号的时候能够调用函数,并通过函数中英语善后的代码,原计划的退出。

​ 我们也可以向其发送0的信号来确保程序是否存活

示例代码:

#include <iostream>
#include <signal.h>

using namespace std;

void Exit(int signum)
{
    cout<<"收到了"<<signum<<"信号"<<endl;
    cout<<"开始释放资源并退出"<<endl;
    //释放资源的代码
    cout<<"退出程序"<<endl;
    exit(0);
}
int main()
{
    for(int i=1;i<=64;i++) 
    {
        signal(i,SIG_IGN);
    }
    signal(2,Exit);
    signal(15,Exit);
    while(1)
    {
        cout<<"fengxu\n";
    }
}

进程终止

进程的终止

main()函数中,return的返回值就是终止状态,如果没有return语句或者调用exit(),那么该进程终止状态为0

我们可以通过

echo $?

来查看线程终止的状态

正常终止进程函数有三个:

void exit(int status);
void _exit(int status);
void _Exit(int status);

status也是进程终止的状态

注意:进程如果被异常终止,终止状态也为非0

资源释放

return表示函数返回,会调用局部对象的析构函数,main()函数中的return还会调用全局对象的析构函数

exit()表示进程终止,它不会调用局部对象的析构函数,只会调用全局变量的析构函数

注意:exit()会执行清理工作再退出,但是_EXIT()——exit()不会执行清理工作

进程的终止函数

进程可以利用atexit函数来登记终止函数(最多32个),这些函数将由exit()自动调用。

**注意:**运行登记函数的顺序与登记函数顺序相反

示例代码:

#include <iostream>
#include <stdlib.h>

using namespace std;

void fuc1()
{
    cout<<"调用了fuc1()"<<endl;
}

void fuc2()
{
    cout<<"调用了fuc2()"<<endl;
}

int main()
{
    atexit(fuc1);
    atexit(fuc2);
    exit(0);
}

输出:

在这里插入图片描述

调用可执行程序

system函数

system()函数提供了一种简单的执行程序的方法,把需要执行的参数用一个字符串传给system()函数就行了

函数声明:

int system(const char *string);

返回值:0成功,非0失败

示例代码:

#include <iostream>
#include <stdlib.h>

using namespace std;

int main()
{
    int ret=system("ls -l");
    cout<<ret<<endl;
    perror("system");
    return 0;
}

输出

在这里插入图片描述

exec函数族

前言

我们在用fork()函数去创建一个进程的时候,当我们想继续使用这个进程去去执行其他函数的时候,我们可以去调用exec函数,这样该进程将被替换为全新的程序,而且调用exec函数,前后函数的进程不变

exec函数族函数的功能

它能够在调用进程内部去执行一个可执行文件,它既可以是二进制文件,也可以是任何Linux下的可执行脚本文件

exec函数的返回值

exec函数族的函数在执行成功后不会返回,调用失败则会返回-1并设置error值,并从原程序调用点继续往下执行

exec函数的种类

exec族函数一个有六种:

  1. execl(const char *path, const char *arg, ...):接受一个以NULL结尾的参数列表,第一个参数是要执行的可执行文件的路径,后面的参数是传递给可执行文件的命令行参数。
  2. execlp(const char *file, const char *arg, ...):与execl函数类似,但它在可执行文件的搜索路径中查找该文件,而不是仅使用给定的路径。
  3. execle(const char *path, const char *arg, ..., char *const envp[]):类似于execl函数,但允许指定新的环境变量参数(通过envp数组传递)。
  4. execv(const char *path, char *const argv[]):与execl函数类似,但接受一个以NULL结尾的参数数组来传递命令行参数。
  5. execvp(const char *file, char *const argv[]):与execv函数类似,但在可执行文件的搜索路径中查找文件,而不是仅使用给定的路径。
  6. execvpe(const char *file, char *const argv[], char *const envp[]):与execvp函数类似,但允许指定新的环境变量参数

对exec参数的说明

path:可执行文件的路径

arg:可执行文件所带的参数,第一个为文件的名字,不带路径且以NULL结尾

file:如果参数file中带有\,则将其视作路径处理,否则即在当前PATH环境变量按照其指定的各个目录去搜寻可执行文件

exec族函数的分类

exec族函数参数比较难记忆,但是当我们可以通过函数名中的字符来辅助我们记忆

  • l:使用参数列表
  • P:使用文件名,并从PATH环境中寻找可执行文件
  • V:构造一个指向各参数的指针数组,将数组的地址作为这些

函数的参数

  • e:多了envp数组,利用写的环境变量代替了进程的环境变量

接下来是对每种类型的具体描述:

l类函数

带l的一类exac函数(l表示list),包括execlexeclpexecle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。

execl函数为例

//echoarg.cpp
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
  for(int i=0;i<argc;i++)
  {
    printf("argv[%d]=:%s\n",i,argv[i]);
  }
  return 0;
}
//execl.cpp
#include <iostream>
#include <unistd.h> 

using namespace std;

int main()
{
    cout<<"before execl"<<endl;
    if(execl("/home/lib/项目课源码/app/进程与通信/exec族函数/out/echoarg","echorag","abc",NULL)==-1)
    {
        cout<<"execl error"<<endl;
    }
    cout<<"after execl"<<endl;
}

输出结果:
在这里插入图片描述

说明

我们先用g++编译echoarg.cpp,生成可执行文件echoarg并放在目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

p类函数

带p的一类exac函数,包括execlpexecvpexecvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    cout<<"before execlp\n";
    if(execlp("ls","ls","-l",NULL)==-1)
    {
        cout<<"execlp error\n";
    }
    cout<<"after execlp\n";
    return 0;
}

带v不带l的函数

带v不带l的一类exac函数,包括execvexecvpexecve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

以下面代码作为例子

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    cout<<"before execvp";
    char *argv[]={"ls","-l",NULL};//最后一个元素必须为NULL
    if(execvp("ls",argv)==-1)
    {
        cout<<"execvp error"<<endl;
    }
    cout<<"after execvp";
    return 0;
}

输出结果:

在这里插入图片描述

e类函数

带e的一类exac函数,包括execleexecvpe,可以传递一个指向环境字符串指针数组的指针。

进程

进程的创建

整个Linux系统全部的进程是一个树状结构

0号进程(系统进程):所有进程的祖先,创建了1号进程与2号进程

1号进程(systemd):负责执行内核的初始化工作与继续系统配置

2号进程(kthreadd):负责所有内核线程的调度与管理

我们可以使用pstree命令来查看进程树,命令为:

pstree -p 进程编号

示例:

在这里插入图片描述

进程标识

每一个进程的有一个非负整数标识的唯一的进程ID,虽然是唯一,但是我们可以复用进程ID,当一个进程终止以后,该进程ID自然就成为了复用的候选者,但Linux本身采用的是延迟服用算法,让新建进程的ID不同于最近计数进程的ID,防止被误以为进程尚未终止

pid_t getpid(void) //获取当前进程的ID
pid_t getppid(void) //获取父进程的ID

说明:

pid_t:非负整数

进程的创建

fork函数

一个现有的进程能够调用fork()函数去创建一个新的进程

pid_t fork(void);

fork()创建的进程叫做子进程,下面演示一个例子:

//demo1.cpp
#include <iostream>
#include<unistd.h>

using namespace std;

int main()
{
    fork();
    cout<<"hello world"<<endl;
    sleep(100);
    cout<<"over";
    return 0;
}

我们运行一下结果如下:

在这里插入图片描述

我们使用命令来查看一下它的进程

ps -ef |grep demo1

在这里插入图片描述

我们用上面的查看进程树命令来试一下:

pstree -p 214251

在这里插入图片描述

我们可以看到它创建了一个子进程

分割子进程与父进程

fork()会返回值,而子进程与父进程的返回值不同,示例代码如下:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    fork();
    int pid=fork();
    cout<<pid<<endl;
}

输出结果:

在这里插入图片描述

我们可以发现:

子进程的返回值:0

父进程的返回值:父进程的进程ID

所以我们可以通过这个来选择父进程与子进程所执行的代码

示例代码:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    int pid = fork();
    if(pid==0)  cout<<"现在执行的是子进程"<<endl;
    if(pid>0)  cout<<"现在执行的是父进程"<<endl;
}

输出结果:

在这里插入图片描述

子进程与父进程之间的关系

在子进程被创建之后,它与父进程之间并不是共享堆栈以及数据空间的而是子进程获得了父进程的数据空间以及堆栈的副本

fork()的两种用法

  1. 父进程复制自己,然后父进程与子进程分别执行不同的代码,多见于网络服务程序,父进程等待客户端的连接请求,当请求到达的时候,父进程调用fork(),让子进程处理请求,而父进程等待下一个连接请求
  2. 进程需要执行另一个程序,这种多见于shell中,让子进程去执行exec族函数

共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程与子进程共享一个文件偏移量(子进程所写的内容会在父进程所写内容的后面)

注意:如果父进程与子进程写同意描述符指向的文件,但是每一然后显示的同步,那么它们的输出可能相互混合

vfork()函数

vfork()函数的调用与fork函数相同,但两者的语义不同

vfork()函数用于创建一个新进程,而新进程的目的是exec一个新程序,由于我们要求子进程必须立即执行,所以它不复制父进程的地址空间

vfork()fork()的宁一个区别:vfork()保证子进程先执行,保证了子进程调用exec函数或exit()之后父进程才恢复执行

僵尸进程

前言

如果父进程比子进程先退出,子进程将被1号进程所托管(这是一种让进程在后台运行的方法),而如果子进程比父进程先退出,且父进程并没有处理子进程退出的信息的话,那么子进程将成为僵尸进程。

代码示例:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    if(fork()==0) return 0;
    for(int i=0;i<1000;i++)
    {
        cout<<"hello world"<<endl;
        sleep(100);
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以看到哪怕子进程已经退出了,但是我们查找进程的时候,子进程依旧存在,这时候它就成为了一个僵尸进程。

僵尸进程的危害

Linux内核给每一个子进程都保留了一个数据结构,它包括了进程编号,终止状态,使用cpu时间等等。当父进程处理了子进程的退出之后内核会将这个数据结构释放掉,而父进程如果没有将子进程的退出处理掉,内核就不会释放这个数据结构,这样会导致子进程的基础编号一直被占用,而进程编号的数量是有限的,这样将影响系统去创建新的进程

如何避免僵尸进程

  • 子进程退出的时候,内核需要向父进程发出SIGCHLD信号,如果父进程用signal(SIGCHLD,SIG_INT)来表示对子进程的退出不做处理,内核将自动释放子进程的数据结构

  • 父进程通过wait/waitpid函数等待子进程结束,子进程退出前,父进程将被阻塞

     pid_t wait(int *stat_loc);
     pid_t waitpid(pid_t pid, int *stat_loc, int options);
     pid_t wait3(int *stat_loc, int options, struct rusage *rusage);
     pid_t wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage);
    

    返回值是子进程的编号

    变量的说明

    • pid_t pid:要等待的进程的进程ID。
    • int *stat_loc:用于保存进程退出状态的指针。如果不关心进程的退出状态,可以传递 NULL
    • int options:等待选项,可用于指定等待行为的一些附加选项。常见的选项包括 WNOHANG (非阻塞等待)和 WUNTRACED (等待暂停子进程状态)。
    • struct rusage *rusage:用于保存子进程资源使用情况的结构体指针。如果不关心子进程的资源使用情况,可以传递 NULL
    • stzt_loc是子进程终止的信息,如果是正常终止,宏WIFEEXITED(stat_loc)返回真,WEXITSTAUTS(stat_loc)可获取终止状态,如果是异常状态,宏WTERMSIG可获取终止进程的信号

    我们来用一段代码实验一下上述知识点:

    #include <iostream>
    #include<unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    using namespace std;
    
    int main()
    {
        //父进程
       if(fork()>0)
       {
            int sts;
            pid_t pid=wait(&sts);
            cout<<"已经终止子进程的进程编号为:"<<pid<<endl;
            if(WIFEXITED(sts))
            {
                cout<<"子进程正常退出"<<"子进程的退出状态为"<<WEXITSTATUS(sts)<<endl;
            }
            else
            {
                cout<<"子进程异常退出"<<"子进程的退出状态为"<<WTERMSIG(sts)<<endl;
            }   
       }
       //子进程
       else
       {
            sleep(30);
            cout<<"byebye"<<endl;
            exit(1);
       }
    }
    

在这里插入图片描述

我们如果尝试使用kill指令去强行结束子进程:

在这里插入图片描述

在这里插入图片描述

  • 如果父进程很忙,我们可以考虑捕获SIGCHLD信号,在信号处理函数里面调用wait()/waitpid()

    代码示例:

    #include <iostream>
    #include <unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    using namespace std;
    
    void  func(int signal)
    {
        int sts;
        pid_t pid=wait(&sts);
        cout<<"子进程pid为"<<pid<<endl;
        if(WIFEXITED(sts))
        {
            cout<<"子进程正常退出\n"<<"子进程的退出状态为"<<WEXITSTATUS(sts)<<endl;
        }
        else
        { 
            cout<<"子进程异常退出\n"<<"子进程的退出状态为"<<WTERMSIG(sts)<<endl;
        }   
    }
    int main()
    {
        signal(SIGCHLD,func);
        if(fork()>0)
        {
            while(true)
            {
                sleep(1);
                cout<<"父进程忙碌中"<<endl;
            }
        }
        else
        {
            sleep(10);
            int *p=0;
            *p=10;
            exit(1);
        }
    }
    

    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/622950.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

栈和队列的基础知识,C语言实现及经典OJ题

题目来源&#xff1a;力扣 基础知识 一.栈 1.栈的概念 定义&#xff1a;堆栈又名栈&#xff08;stack&#xff09;&#xff0c;它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶&#xff0c;相对地&#xff0c;把另一端称为栈底。 压…

【数据结构】栈和队列OJ面试题

20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;由于C语言没有栈的接口&#xff0c;所以我们需要自己造一个“模子”。我们直接copy之前的实现的栈的接口就可以了&#xff08;可以看我之前的博客【数据结构】栈和队列-CSDN博客copy接口&#xff09;&…

OpenSSL自签证书并基于Express搭建Web服务进行SSL/TLS协议分析

OpenSSL自签证书并基于Express搭建Web服务进行SSL/TLS协议分析 起因 最近在学习安全协议&#xff0c;大多数实验都是基于Windows下IIS&#xff0c;或者Linux下nginx搭建的Web服务&#xff0c;搭建环境和编写配置文件比较麻烦。而且我有多个不同环境的设备&#xff0c;折腾起来…

Hive-拉链表的设计与实现

Hive-拉链表的设计与实现 在Hive中&#xff0c;拉链表专门用于解决在数据仓库中数据发生变化如何实现数据存储的问题。 1.数据同步问题 Hive在实际工作中主要用于构建离线数据仓库&#xff0c;定期的从各种数据源中同步采集数据到Hive中&#xff0c;经过分层转换提供数据应用…

计算机视觉的应用30-基于深度卷积神经网络CNN模型实现物体表面缺陷检测技术的项目

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用30-基于深度卷积神经网络CNN模型实现物体表面缺陷检测技术的项目主要包括&#xff1a;物体表面缺陷检测技术项目介绍&#xff0c;数据构造&#xff0c;模型介绍。 物体表面缺陷检测技术是工业自动化…

四川汇聚荣:做拼多多网点需要具备什么能力?

做拼多多网点需要具备什么能力?这个问题对于想要在电商平台上开店的商家来说&#xff0c;是必须要了解的。拼多多作为国内领先的社交电商平台&#xff0c;吸引了众多商家入驻。那么&#xff0c;要想在拼多多上开网店&#xff0c;需要具备哪些能力呢?下面就从四个方面进行详细…

Android Cursor与Adapter结合使用

查询数据库均会把查询的结果包装在一个Cursor的子类对象中返回。Cursor就像是位于结果集之上的一个游标&#xff0c;可以对结果集进行向前、向后或随机的访问。而Cursor本身是一个接口类&#xff0c;提供了对结果集访问的一些抽象方法&#xff0c;根据功能的不同在其子类有着不…

Python | Leetcode Python题解之第88题合并两个有序数组

题目&#xff1a; 题解&#xff1a; class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:"""Do not return anything, modify nums1 in-place instead."""p1, p2 m - 1, n - 1tail m n - 1whi…

elasticsearch 动态映射

文章目录 动态映射动态映射的弊端静态映射实战&#xff1a;映射创建后还可以更新吗 动态映射 动态映射的核心是在自动检测字段类型后添加新字段 哪些字段类型支持动态检测呢&#xff1f; 答&#xff1a;boolean类型、float类型、long类型、Object类型、Array类型、date类型、…

MQTT学习(一)

MQTT是一种与HTTP类似的应用层协议。 在某些物联网应用中&#xff0c;MQTT优于HTTP。 首先&#xff0c;HTTP是用于客户端服务器计算的以文档为中心的请求-响应协议。 HTTP是万维网的基础&#xff0c;但它不是专门为机器之间通信而设计的。 MQTT是一种机器对机器、以数据为中…

重学java 37.多线程基本了解

尽管走自己的路&#xff0c;别被那些三言两语击倒 —— 24.5.13 一、多线程_线程和进程 进程&#xff1a;在内存中执行的应用程序 线程:是进程中最小的执行单元线程作用:负责当前进程中程序的运行,一个进程中至少有一个线程,一个进程还可以有多个线程,这…

Automa:一键自动化,网页数据采集与工作流程优化专家

Automa&#xff1a;解锁自动化浏览器潜能&#xff0c;赋能工作效率&#xff0c;让复杂任务变得简单- 精选真开源&#xff0c;释放新价值。 概览 Automa是一款创新的网页自动化工具&#xff0c;专为寻求提升工作效率、简化数据收集过程的现代工作者设计。它融合了先进的数据抓取…

EasyExcel 中实体类的注解@ExcelProperty

ExcelProperty(value "职务", index 0) value 与index 的优先级, 实测得出下面结论. 1、只有value : 按照value 的匹配 2、只有index: 按照index 的匹配 3、 同时有value和index: 按照index的匹配. 结果: 按照index的匹配, 找到的数据 {"administrat…

GO—web程序中的请求缓存设置

背景 假设用户数据存在数据库&#xff0c;现在需要一个函数&#xff0c;通过用户名称返回用户信息。 期望&#xff1a;在一次web请求中&#xff0c;不过调用多少次这个函数&#xff0c;只请求一次数据库。 基本信息 type User struct {Name stringAge int }func GetALLUser…

服务器3389端口,服务器3389端口风险提示的应对措施

3389端口是Windows操作系统中远程桌面协议&#xff08;RDP&#xff09;的默认端口。一旦该端口被恶意攻击者利用&#xff0c;可能会导致未经授权的远程访问和数据泄露等严重安全问题。 针对此风险&#xff0c;强烈建议您采取以下措施&#xff1a; 1. 修改默认端口&#xff1a;…

苹果手机系统恢复工具:轻松解决iPhone各类系统问题!

随着苹果手机的iOS系统不断升级&#xff0c;越来越多的系统问题不断出现&#xff0c;如卡在恢复模式、系统崩溃白苹果、应用无响应、等&#xff0c;这些问题不仅影响用户体验&#xff0c;还可能导致手机无法正常使用。 遇到系统问题&#xff0c;一般我们可以先尝试使用强制重启…

【原创】springboot+mysql校园宿舍报修管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

景源畅信:小白如何做抖音电商怎么样?

在数字浪潮中崛起的抖音电商&#xff0c;以其独特的平台优势吸引了众多创业者的目光。特别是对于初入电商领域的“小白”来说&#xff0c;如何在这个全新的领域站稳脚跟&#xff0c;成为他们迫切需要解答的问题。接下来&#xff0c;我们将深入探讨小白如何在抖音电商中开辟属于…

免费思维13招之十:增值型思维

免费思维13招之十:增值型思维 免费思维的另一大战略思维——增值型思维。 为了提高客户的粘性而促进重复性消费,我们必须对客户进行免费的增值型服务。 大家不要把增值型思维与赠品型思维混淆,增值型思维重心在于提高与消费者的粘性而促进重复消费,重心在后端。而赠品型思…

Spring Cloud Alibaba 分布式配置中心(9)

项目的源码地址 Spring Cloud Alibaba 工程搭建&#xff08;1&#xff09; Spring Cloud Alibaba 工程搭建连接数据库&#xff08;2&#xff09; Spring Cloud Alibaba 集成 nacos 以及整合 Ribbon 与 Feign 实现负载调用&#xff08;3&#xff09; Spring Cloud Alibaba Ribbo…