【Linux】信号

Linux 信号

  • 1.信号介绍
  • 2.core dump
  • 3.发送信号
    • 3.1.kill
    • 3.2.send
    • 3.3.abort
  • 4.信号产生
    • 4.1.软件条件产生信号
      • 4.1.1.SIGPIPE
      • 4.1.2.SIGALRM
    • 4.2.硬件异常产生信号
  • 5.信号处理
  • 6.可重入函数 & volatile
  • 7.SIGCHLD

1.信号介绍

信号本质是一种通知机制。
而进程要处理信号,必须具备“识别”信号的能力(能看到+能处理)。
一般而言,信号的产生相对于进程而言是异步的。信号随机产生,进程可能不能立即处理,所以进程要能够临时地记录下信号,以便后续合适的时候进行处理。

如何理解信号被进程保存?
进程要能知道是什么信号,以及是否产生
进程具有保存信号的相关数据结构(位图),在进程PCB内部保存了信号的位图字段。

如何理解信号发送的本质?
操作系统向目标进程写信号,即操作系统直接修改进程PCB中的指定的位图结构,完成“发送”信号的过程。

对于信号处理的常见的有三种方式:

  • 默认(SIG_DFL)
  • 忽略(SIG_IGN)
  • 自定义动作(捕捉信号)

在这里插入图片描述

void handler(int signum)
{
    cout << "["<< getpid() << "] handler正在处理signum: " << signum << endl;
}

void test1()
{
    signal(SIGINT, handler); // 特定信号的处理动作,一般只有一个
    // signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作

    while(true)
    {
        cout << "["<< getpid() << "] test正在运行中..." << endl;
        sleep(1);
    }
}

在这里插入图片描述
在这里插入图片描述
如何理解键盘组合键形成信号?
键盘的工作方式是通过中断的方式进行的。
操作解释组合键信息,查找进程列表找到前台运行的进程,然后写入对应的信号到进程内部的位图结构中。

Linux的所有信号可以通过kill -l指令查看。
在这里插入图片描述
如图所示,[1, 31]是普通信号,[34, 64]是实时信号。man 7 signal可以查看信号的具体信息。

2.core dump

核心转储:
一般而言,云服务器(生产环境)的核心转储功能是被关闭的。
在这里插入图片描述
core dump标记位:
core dump标志,用于标志是否发生核心转储。是在进程发生某种异常的时候,由操作系统将当前进程在内存中的核心数据转储到磁盘中(表现是生成了一个core.pid的文件),可以便于调试使用。

void test2()
{
    while(true)
    {
        cout << "["<< getpid() << "] test正在运行中..." << endl;
        sleep(1);

        int a = 1 / 0;

        cout << "hello world" << endl;
    }
}

在这里插入图片描述
使用core文件可以在gdb调试环境下快速定位出程序出错的地方。(SIGFPE-8号信号)
在这里插入图片描述
还有很多信号的行为有core dump标记位的设置。
在这里插入图片描述

void test3()
{
    pid_t pid = fork();
    if(pid == 0)
    {
        cout << "I am child" << endl;
        int a = 1 / 0;
        exit(1);
    }

    int status = 0;
    waitpid(pid, &status, 0);
    cout << "father pid: " << getpid() << " | " << "child pid: " << pid << \
    " | " << "child exit signal: " << (status & 0x7f) << " | " << "child core dump: " << ((status >> 7) & 1) << endl;
}

在这里插入图片描述

3.发送信号

如何理解发送信号的系统调用接口?
用户调用系统接口,操作获取参数,向目标进程写对应信号,即修改目标进程的信号标记位,进程后续对信号做出处理动作。

3.1.kill

在这里插入图片描述

// 简单模拟kill接口来实现kill指令

static void Use(const string& proc)
{
    cout << "Usage:\r\n\t" << proc << " signum pid" << endl;
}

// ./mykill signum pid
void test4(int argc, char* argv[])
{
    if(argc != 3)
    {
        Use(argv[0]);
        exit(1);
    }

    int signum = atoi(argv[1]);
    pid_t pid = atoi(argv[2]);

    kill(pid, signum);
}

在这里插入图片描述

3.2.send

在这里插入图片描述

void test5()
{
    cout << "I am running..." << endl;
    sleep(1);
    raise(9);
}

在这里插入图片描述

3.3.abort

在这里插入图片描述
在这里插入图片描述

void test6()
{
    cout << "I am running..." << endl;
    sleep(1);
    abort(); // 通常用来终止进程
}

在这里插入图片描述

4.信号产生

4.1.软件条件产生信号

对于管道文件,当读端不仅不读,还关闭了,那么写端再写也就没有意义了。操作系统会自动中断对应的写端进程(通过发送信号SIGPIPE的方式)。

4.1.1.SIGPIPE

在这里插入图片描述

/*
* SIGPIPE 信号验证
* 1.创建匿名管道
* 2.父进程读,子进程写
* 3.父子进程可以通信一段时间(非必要)
* 4.父进程关闭读端 && waitpid(),子进程一直写
* 5.子进程会退出,父进程拿到子进程退出的status,提取退出信号
*/

void Test1()
{
    // 创建管道
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    if(ret != 0)
    {
        perror("pipe");
        exit(1);
    }

    // 创建子进程
    pid_t pid = fork();

    if(pid > 0)
    {
    	// 构建单向通行的信道,父进程读,子进程写
        // 父进程 -- 读
        
        // 关闭写端
        close(pipefd[1]);

        // 不断读取信息
        char receive[128] = {0};
        while(true)
        {
            ssize_t size = read(pipefd[0], receive, 127);
            if(size > 0)
            {
                cout << "father receive: " << receive << endl;
                if(strcmp(receive, "hello world5") == 0)
                {
                    // 关闭读端
                    close(pipefd[0]);
                    break;
                }
            }
            else if(size == 0) 
            {
                break;
            }
            else
            {
                perror("read");
                exit(4);
            }
        }
		
		// 等待子进程
        int status = 0;
        pid_t wpid = waitpid(pid, &status, 0);
        if(wpid == -1)
        {
            perror("waitpid");
            exit(3);
        }
        cout << "father exit code: " << (status & 0x7f) << " | " << "father core dump: " << ((status >> 7) & 1) << endl;
    }
    else if(pid == 0)
    {
        // 子进程 -- 写
		
		// 关闭读端
        close(pipefd[0]);

        int count = 0;
        while(true)
        {
        	// 不断写变化的信息
            string msg = "hello world" + to_string(count++);
            write(pipefd[1], msg.c_str(), msg.size());
            sleep(1);
        }
    }
    else 
    {
        perror("fork");
        exit(2);
    }
}

在这里插入图片描述
管道是软件,因为管道是文件在内存级的实现。像这种读端关闭,写端还在写,就属于软件条件不满足的情况,此时操作系统就会向写端进程发送SIGPIPE信号。

4.1.2.SIGALRM

在这里插入图片描述
在这里插入图片描述

// 定时器功能
uint64_t count = 0;
vector<function<void()>> v;

void showCount()
{
    cout << "final count: " << count << endl;
}

void logUser()
{
    if(fork() == 0)
    {
        execl("/usr/bin/who", "who", nullptr);
        exit(1);
    }

    wait(nullptr);
}

void handler(int signum)
{
    (void)signum;
    for(auto& f : v)
    {
        f();
    }
    alarm(1);
}

void test7()
{
    alarm(1); // 时间一到就会进行时钟中断,向进程发送时钟信号
    signal(SIGALRM, handler);

    v.emplace_back(showCount);
    v.emplace_back(logUser);

    while(++count);
}

在这里插入图片描述
如何理解软件条件给进程发送信号?
操作系统先识别到某种软件条件触发或者不满足,然后构建信号,发送给指定进程。

4.2.硬件异常产生信号

如何理解除0操作?
计算机中做计算的是CPU这个硬件,CPU内部是有寄存器的,包括状态寄存器(位图结构),有对应的状态标记位。操作系统会自动进行计算完毕之后的检测(状态寄存器的检测),如果识别到有问题,就去找当前在运行进程的pid,向其发送信号,进程后续会做出处理。

如何理解野指针或越界问题?
无论是野指针还是越界访问,都是必须通过地址,找到目标位置。
语言层面的地址,都是虚拟地址,要将其转化成物理地址(页表+硬件MMU[Memory Management Unit])。
野指针,越界都属于非法地址,MMU在转化的时候会报错。(不只是CPU中有寄存器,几乎所有外设和常见的硬件,都可能存在寄存器,所以MMU中也是有寄存器的)

void handler(int signum)
{
    cout << "signum: " << signum << endl;
    sleep(1);
}

void test8()
{
    signal(SIGFPE, handler);
    int a = 1 / 0;

    while(true) sleep(1);
}

void test9()
{
    signal(SIGSEGV, handler); // 11) SIGSEGV - Invalid memory reference

    int* p = nullptr;
    *p = 1;
    
    while(true) sleep(1);
}

上面程序为什么死循环?
因为寄存器中的异常一直没有得到解决。

所有的信号,都有它的来源,但最终都是会被操作系统识别,解释,并发送的。

5.信号处理

一个信号被处理,是怎样的一个过程呢?
在这里插入图片描述
handler是一个函数指针数组,数组的下标和信号的编号相对应。
block位图,结构和pending位图一样,block位图的含义是对应信号是否被屏蔽阻塞。
操作系统不允许用户直接进行位图的操作,但提供了操作位图的方法。
在这里插入图片描述

  • sigemptyset & sigfillset用于初始化set信号集。
  • sigaddset & sigdelset用于设置特定信号signum的比特位信息。
  • sigismember用于判断特定信号signum是否在set信号集中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果把所有的信号都进行自定义捕捉,进程会怎样?

static void handler(int signum)
{
    cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}

void test10()
{
    // 9 & 19 信号仍有效
    for(int signum = 1; signum <= 31; ++signum)
    {
        signal(signum, handler);
    }

    cout << "pid: " << getpid() << endl;
    while(true) sleep(1);
}
# sendSignal.sh
# chomd +x 添加执行权限
i=1
pid=$(pidof test)
while [ $i -le 31 ]
do
    if [ $i -eq 9 -o $i -eq 19 ]
    then
        let ++i
        continue
    fi
    kill -$i $pid
    echo "kill -$i $pid"
    let ++i
    sleep 1
done

在这里插入图片描述
如果把所有的信号都进行block,不断获取并打印当前进程的pending信号集,进程会怎样?

static void handler(int signum)
{
    cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}

static void showPending(const sigset_t& pending)
{
    for(int signum = 1; signum <= 31; ++signum)
    {
        if(sigismember(&pending, signum))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void test11()
{
    // 0.捕捉2号信号,以便测试
    signal(2, handler);

    // 1.定义信号集对象(位图)
    sigset_t bset, obset;
    sigset_t pending;

    // 2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);

    // 3.添加要进行屏蔽的信号
    sigaddset(&bset, 2 /*SIGINT*/);

    // 4.设置bset到内核中对应的进程PCB内[默认情况下进程不会对任何信号进行block]
    int ret = sigprocmask(SIG_BLOCK, &bset, &obset);
    if(ret == -1)
    {
        perror("sigprocmask");
        exit(1);
    }
    cout << "[" << getpid() << "] " << "block signum 2 success" << endl;

    // 5.重复打印当前进程的pending信号集
    int count = 0;
    while(true)
    {
        // 5.1.获取当前进程的pending信号集
        sigpending(&pending);
        // 5.2.显示pending信号集中没有被递达的信号
        showPending(pending);

        sleep(1);
        ++count;
        if(count == 10)
        {
            int ret = sigprocmask(SIG_SETMASK, &obset, nullptr);
            if(ret == -1)
            {
                perror("sigprocmask");
                exit(2);
            }
            cout << "[" << getpid() << "] " << "recover signum 2 success" << endl;
        }
    }
}

在这里插入图片描述

static void blockSignal(int signum)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, signum);

    int ret = sigprocmask(SIG_BLOCK, &bset, nullptr);
    if(ret == -1)
    {
        perror("sigprocmask");
        exit(1);
    }
}

void test12()
{
    // 9 & 19 信号仍有效
    for(int signum = 1; signum <= 31; ++signum)
    {
        blockSignal(signum);
    }

    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        showPending(pending);

        sleep(1);
    }
}

在这里插入图片描述
SIGKILL/SIGSTOP信号 无法被自定义,无法被阻塞,无法被忽略。
在这里插入图片描述
在这里插入图片描述
这里主要使用sigaction中的第一个和第三个参数。

void showPending(sigset_t* pending)
{
    for(int signum = 1; signum <= 31; ++signum)
    {
        if(sigismember(pending, signum)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}

void handler(int signum)
{
    cout << "[" << getpid() << "] " << "signum: " << signum << endl;

    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        sleep(1);
    }
}

void test13()
{
    // 内核数据类型,在用户栈空间定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;

    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 6);
    sigaddset(&act.sa_mask, 7);

    // 设置进当前进程的PCB中
    sigaction(2, &act, &oact);
    cout << "default action: " << (int)oact.sa_handler << endl;

    while(true) sleep(1);
}

在这里插入图片描述
在这里插入图片描述
处理信号的时候,执行自定义的动作,如果在处理信号期间,又来了同样的信号,操作系统又该如何处理?操作系统的处理方式可以让我们意识到block信号集存在的道理。

信号产生之后,可能无法立即被处理,而是在合适的时候处理。
至于合适的时候,是从内核态返回用户态的时候,进行信号的检测和处理。
在这里插入图片描述
内核级页表,可以被所有进程看到。
用户态是一个受管控的状态(权限有限)。内核态是一个操作系统执行自己代码的状态,具备很高的优先级。
用户态可以通过系统调用接口,异常等进入内核态,这也不过是从用户地址空间跃迁到内核地址空间之后,执行操作系统自己的代码。所以,内核也是在所有进程地址空间的上下文中跑的。
所有的程序指令都要经由CPU来执行,CPU又是如何来区分内核态和用户态的呢?
CPU中的CR3寄存器用于表示当前CPU的执行权限是用户级还是内核级。

信号处理的整个流程?
在这里插入图片描述
信号处理的整体可以分为两个层级,四次切换。
如果操作系统是执行SGI_DFLSGI_IGN的处理,内核级就可以完成,但是如果处理自定义捕捉handler的动作,就需要到用户级去处理(只有自定义处理方式的信号会在用户态进行处理)。当然内核级的权限很高,有能力将自定义捕捉动作在内核级就处理掉,但是操作系统并不会这样做。因为操作系统是不相信用户的,不相信用户所写的代码的。

6.可重入函数 & volatile

  1. 可重入函数:
    信号捕捉,并没有创建新的进程或者线程。
    函数是否可重入的关键在于函数内部是否对全局数据进行了不受保护的非原子操作,其中原子操作指的是一次完成,中间不会被打断的操作,表示操作过程是安全的。
    如果用户级程序在访问某一全局资源时,正好陷入内核,然后从内核态返回到用户态时进行信号检测,进行handler方法处理,此时如果同样访问这一资源,就可能出现冲突。能导致这类问题发生的函数便是不可重入函数。
    可重入函数 和 不可重入函数 是函数的一种特征,并没有对错之分。

  2. volatile:
    让CPU保持内存的可见性。

void test14()
{
    volatile const int i = 0;
    int* pi = (int*)&i;
    *pi = 1;

    cout << "i: " << i << endl;
    cout << "*pi: " << *pi << endl;
}

在这里插入图片描述

7.SIGCHLD

在这里插入图片描述

void handler(int signum)
{
    cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}

// 证明子进程退出,会向父进程发送信号
void test15()
{
    signal(SIGCHLD, handler);
    if(fork() == 0)
    {
        cout << "child pid: " << getpid() << endl;
        sleep(3);
        exit(0);
    }
    while(true) sleep(1);
}

在这里插入图片描述

// 不等待子进程,并且让子进程退出后,自动释放
void test16()
{
    signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略

    if(0 == fork())
    {
        cout << "child: " << getpid() << endl;
        sleep(5);
        exit(0);
    }

    while(true)
    {
        cout << "father: " << getpid() << endl;
        sleep(1);
    }
}

在这里插入图片描述

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

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

相关文章

Docker Swarm总结+基础、集群搭建维护、安全以及集群容灾(1/4)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

PyQt6 QLabel标签控件

​锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计21条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话…

jQuery_08 each函数的使用

each函数的使用 可以循环数组&#xff0c;json&#xff0c;dom对象数组 1.$.each(要循环的内容,function(index,element){处理函数}) 要循环的内容可以是数组&#xff0c;json对象&#xff0c;dom数组 function&#xff1a;循环的处理函数 每个成员都会执行这个函数一次 index&…

5.golang字符串的拆解和拼接

字符串是 Go 中的字节切片。可以通过将一组字符括在双引号中来创建字符串" "。Go 中的字符串是兼容Unicode编码的&#xff0c;并且是UTF-8编码的。 访问字符串的单个字节或字符 由于字符串是字节切片&#xff0c;因此可以访问字符串的每个字节。 func printStr(s …

Spring Boot 项目中读取 YAML 文件中的数组、集合和 HashMap

在 Spring Boot 项目中&#xff0c;我们经常使用 YAML 文件来配置应用程序的属性。在这篇博客中&#xff0c;我将模拟如何在 Java 的 Spring Boot 项目中读取 YAML 文件中的数组、集合和 HashMap。 1. 介绍 YAML&#xff08;YAML Aint Markup Language&#xff09;是一种人类…

建造者模式-C语言实现

UML类图&#xff1a; 代码实现&#xff1a; #include <stdio.h> #include <stdlib.h>// 产品类 typedef struct {char* part1;char* part2;char* part3; } Product;// 抽象建造者类 typedef struct {void (*buildPart1)(void*, const char*);void (*buildPart2)(v…

2023-3年CSDN创作纪念日

机缘 今天开开心心出门去上班&#xff0c;就收到了一个csdn私信&#xff0c;打开一看说是给我惊喜来着&#xff0c;我心想csdn还能给惊喜&#xff1f;以为是有什么奖品或者周边之类的&#xff0c;结果什么也没有&#xff0c;打开就是一份信&#x1f602;。 也挺不错的&#xf…

java: nio之DirectByteBuffer

package nio;import java.nio.ByteBuffer; import java.nio.IntBuffer;public class DirectTest {public static void main(String[] args) {ByteBuffer byteBuffer ByteBuffer.allocateDirect(1024);} }

Android 相机库CameraView源码解析 (一) : 预览

1. 前言 这段时间&#xff0c;在使用 natario1/CameraView 来实现带滤镜的预览、拍照、录像功能。 由于CameraView封装的比较到位&#xff0c;在项目前期&#xff0c;的确为我们节省了不少时间。 但随着项目持续深入&#xff0c;对于CameraView的使用进入深水区&#xff0c;逐…

由于找不到vcruntime140.dll无法继续执行代码-提供5个修复方法分你对比

摘要&#xff1a;本文将介绍vcruntime140.dll文件的作用及其在程序运行中的重要性&#xff0c;并提供五个解决vcruntime140.dll无法继续执行的方法。 一、vcruntime140.dll文件介绍 vcruntime140.dll是Windows操作系统中的一项重要文件&#xff0c;它是由Microsoft Visual C提…

『OPEN3D』1.8 点云的配准理论

点云的配准是将不同的3D点云对齐成一个完成的点云模型&#xff1b;配准的目标是找到两帧点云之间的相对旋转&#xff08;rotation&#xff09;与平移&#xff08;translation&#xff09;&#xff0c;使得两份点云中有重叠的区域能够完好拼接。 点云配准示例图&#xff08;来自…

NX二次开发UF_CURVE_ask_joined_parms 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_ask_joined_parms Defined in: uf_curve.h int UF_CURVE_ask_joined_parms(tag_t joined_curve_feature, UF_STRING_p_t uf_curve_string, int * creation_method, double …

Windows TCP 通信测试_1

一、单对单通信测试 应用函数 socket、bind、connect、listen、accept、recv、send&#xff08;win下的函数&#xff09;等 1、客户端demo client.cpp #include<WINSOCK2.H> #include<STDIO.H> #include<iostream> #include<cstring> using namespa…

【C++】多线程(一):std::thread的使用

这篇文章应我朋友的邀请&#xff0c;写一篇文章介绍下C多线程。 编译环境准备 首先确定你的编译器支持std的thread&#xff0c;如果不支持&#xff0c;就会出现诸如“thread找不到”的问题。 以下假设你使用 gnu gcc 编译器&#xff0c;因为 MSVC 的我也不太熟悉。 linux …

【挑战业余一周拿证】二、在云中计算 - 第 1 节 - 模块2 简介

第 1 节 - 模块2 简介 无论你的企业是属于像医疗、保健、制造、保险等等行业 , 再或者 , 您的服务是向全世界的数百万用户提供视频、、图片或者文字服务,你也需要服务器来为您的业务和应用程序提供支持,服务器的作用是帮助您托管应用程序并提供满足您业务需求的计算能力. 当你使…

机器学习笔记 - 3D对象检测技术路线调研(未完)

一、3D对象检测简述 3D对象检测是计算机视觉中的一项任务&#xff0c;其目标是根据对象的形状、位置和方向在 3D 环境中识别和定位对象。它涉及检测物体的存在并实时确定它们在 3D 空间中的位置。这项任务对于自动驾驶汽车、机器人和增强现实等应用至关重要。 1、基本流程 给定…

leetcode_828_统计子串中的唯一字符

题意&#xff1a;所有子串中单个字符出现的次数和 问题转化&#xff1a;对于串中的每个字符&#xff0c;只包含其一次的所有子串的个数和 关于求只包含某位置字符一次的子串个数 class Solution { public:int uniqueLetterString(string s) {/* ...A...A...A...*/int n s.size…

DDD(领域驱动设计)一些基础概念

DDD、微服务和中台之间的关系 DDD、微服务和中台之间的关系。 中台本质是业务模型&#xff0c;微服务是业务模型的系统落地&#xff0c;DDD 是一种设计思想&#xff0c;可以同时指导中台业务建模和微服务设计&#xff0c;它们之间就是这样的一个铁三角关系。DDD 强调领域模型…

【深度学习笔记】05 线性回归

线性回归 线性回归基于几个简单的假设&#xff1a; 首先&#xff0c;假设自变量 x \mathbf{x} x和因变量 y y y之间的关系是线性的&#xff0c; 即 y y y可以表示为 x \mathbf{x} x中元素的加权和&#xff0c;这里通常允许包含观测值的一些噪声&#xff1b; 其次&#xff0c;我…

Educational Codeforces Round 158 [Rated for Div. 2]

A. Line Trip 还算比较简单的&#xff0c;不过本蒟蒻一开始以为是二分答案&#xff0c;二分写到一半突然想到油量直接取两个加油站之间的最大距离就好了。 最大距离能过&#xff0c;剩下必然都能过&#xff0c;要特判a[n]~x距离是两倍&#xff0c;因为x没有加油站&#xff0c…