【Linux】深入理解文件缓冲区

news2024/10/5 13:51:18

文章目录

      • 问题引入
      • 如何理解缓冲区
      • 缓冲区刷新策略
      • 问题解释
      • 模拟一个文件缓冲区

问题引入

首先看一段代码:

#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg0="hello printf\n";
    const char *msg1="hello fwrite\n";
    const char *msg2="hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}

运行代码,结果如下:

image-20230828114238102

如果此时将输出结果重定向一下:

image-20230828114139893

会发现printffwrite都打印了两次。

究其原因,就要谈到缓冲区和缓冲区刷新策略的概念了。


如何理解缓冲区

假设你在青岛,你要从网上买一件商品,商家所在地是北京。你不会跑去北京自提,商家也不会跑到青岛亲自送货,因为成本又高效率又低。实际做法是商家通过寄快递的方式把商品寄到你所提供的地址附近的菜鸟驿站。

我们把北京比作外设,把商品比作信息,把青岛比作内存,把你比作进程,所以现在的情景就变成了位于内存的进程要从外设中读取一段信息。那此时菜鸟驿站是个什么角色呢?答案是缓冲区。

我们都有一个共识,那就是访问外设的效率是很低的。

为了解决这个问题,内存中会单独开一块空间作为缓冲区,当你想往外设输出数据的时候,很多时候并不会来一条输出一条,而是等缓冲区中数据达到一定数量之后再刷新到外设。同样地,当从外设中读取数据的时候,很多时候也不会只读取你想要的那一条,而是前前后后多读取一部分数据,你下次要读取的数据很大可能就在这部分数据中。这样一来就减少了io的次数,效率也就提高了。

这里就有几个问题需要理解一下:

  1. 将数据刷新到缓冲区或将数据从缓冲区刷新出来的本质是什么?

    按照正常的理解,刷新应该是要把这部分东西挪到另一个地方去,但挪动数据属实有点复杂,一要删除二要拷贝,拷贝数据不可避免,但是删除可以简化。比如可以记录一下缓冲区的当前大小,需要刷新缓冲区的时候,先把缓冲区的数据拷贝出去,然后把缓冲区的大小 置零,此时就完成了惰性删除。如果要把数据刷新到缓冲区,只需要将数据拷贝到缓冲区,然后原数据都不需要动。所以将数据刷新到缓冲区或者从缓冲区中刷新出来的本质是拷贝。更进一步,再加上等待数据刷新的这部分时间,不就是 一次完整的io了么。所以我们还可以得出io的本质就是等待+拷贝。

  2. 缓冲区的本质是什么?

    缓冲区的本质就是一段内存。

  3. 缓冲区是谁提供的 ?

    像我们使用printffwrite这类C语言接口进行输入输出时,都是对文件进行输入输出,而文件在C语言中是以FILE的形式描述组织的,所以对文件输入输出,实际上是向FILE中提供的缓冲区进行拷贝。

  4. 缓冲区是谁申请的?

    缓冲区是用户申请的。缓冲区本质是一段内存,当我们创建FILE对象时其实就会创建一段缓冲区,本质就是用malloc去申请,只不过我们看不到这个过程。

我们可以看一下FILE中的相关字段:

/usr/include/libio.h有一句typedef struct _IO_FILE FILE

我们可以在/usr/include/stdio.h中看到struct _IO_FILE的相关代码,下面是与缓冲区有关的部分字段:

struct _IO_FILE {
    int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
    #define _IO_file_flags _flags
    //缓冲区相关
    /* The following pointers correspond to the C++ streambuf protocol. */
    /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    char* _IO_read_ptr; /* Current read pointer */
    char* _IO_read_end; /* End of get area. */
    char* _IO_read_base; /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr; /* Current put pointer. */
    char* _IO_write_end; /* End of put area. */
    char* _IO_buf_base; /* Start of reserve area. */
    char* _IO_buf_end; /* End of reserve area. */
    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base; /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno; //封装的文件描述符
    #if 0
    int _blksize;
    #else
    int _flags2;
    #endif
    _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
    #define __HAVE_COLUMN /* temporary */
    /* 1+column number of pbase(); 0 is unknown. */
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    /* char* _save_gptr; char* _save_egptr; */
    _IO_lock_t *_lock;
    #ifdef _IO_USE_OLD_IO_FILE
};

缓冲区刷新策略

现在有一个缓冲区,是来一条数据就刷新一条数据,还是等数据达到一定数量才刷新呢?这就跟缓冲区的刷新策略有关了。

在效率方面,上面两种刷新策略无疑是后者效率最高,对于相同数量的数据刷新次数最少,访问外设的次数更少,效率自然更高。那是不是就采用这种刷新策略呢?非也,这还要看对应的场景。

对于显示器这种比较特殊的外设,我们需要时刻获取屏幕上的信息,所以数据从缓冲区刷新到显示器上的频率是会更高的。此时采用的刷新策略就是行缓冲,遇到\n就刷新。这也叫做行缓冲。

当然还有更为激进的刷新策略,就是即使刷新,来一点数据就刷一点。这也叫做无缓冲。

还有一种常见的刷新策略就是全缓冲,等到缓冲区满了再刷新。

所以常见的刷新策略就三条:

  1. 行缓冲,遇到换行符就刷新。
  2. 无缓冲,来一条数据刷新一条数据。
  3. 全缓冲,缓冲区满了才刷新。

当然还有特殊情况,比如进程退出时会把缓冲区刷新一遍,还有就是用户强制刷新,比如调用fflush


问题解释

现在应该能回答最初的问题了。

fprintffwrite都是向stdout中写入,在没有重定向的前提下就是向显示器输出,此时采取的刷新策略是行缓冲,所以会正常打印两条信息。

write是系统调用接口,不存在C语言提供的用户级缓冲区,所以就直接向文件写入了,打印出一条信息。

而一旦进行了重定向,此时输出目标是普通文件,普通文件采取的刷新策略是全缓冲。fprintffwritestdout的缓冲区写入的数据不足以填满缓冲区,所以在两条语句都执行结束之后缓冲区并没有及时刷新。而write由于没有这层缓冲区的缘故直接就写入了。此时创建一个子进程,因为是完全继承的父进程,自然而然地继承了父进程的缓冲区和缓冲区中的数据。下一步子进程和父进程都结束,在两个进程退出之前会先刷新缓冲区,所以两个进程的缓冲区中的数据都被刷新出来,也就是两组相同的字符串,每组两条。也就出现了最终的结果。


模拟一个文件缓冲区

知道了上面的原理,我们可以自己模拟实现一个建议的文件缓冲区及文件操作相关接口,封装一个简易的文件操作,加深对文件操作以及缓冲区的理解。

首先我们需要定义一个简易的FILE结构,因为是一个demo级别的,就封装几个核心字段:

#define SIZE 1024

#define SYNC_NOW    1 << 0  //无缓冲
#define SYNC_LINE   1 << 1  //行缓冲
#define SYNC_FULL   1 << 2  //全缓冲

typedef struct FILE_{
    int flags;        //刷新策略
    int fileno;       //文件描述符
    int cap;          //buffer的总容量
    int size;         //buffer当前的使用量
    char buffer[SIZE];//缓冲区,这里大小是写死的,实际缓冲区的大小是可变的
}FILE_;

此外我们实现四个基本接口,分别是fopen_fclose_fwrite_fflush_

仿照C语言库里函数的参数设计:

FILE_ *fopen_(const char *path_name, const char *mode); 
// path_name是要打开文件的路径
// mode是打开文件时的方式,是读、写还是追加

void fwrite_(const void *ptr, int num, FILE_ *fp);
// ptr是要写入的数据的地址
// num是要写入的数据的字节数
// fp是要向哪个文件中写入

void fclose_(FILE_ * fp);
// fp是要关闭的文件

void fflush_(FILE_ *fp);
// 立即刷新fp的缓冲区

对于fopen_函数,我们直接调用系统调用接口open,在调用之前只要根据mode设置好openflag参数即可,然后需要给文件创建相关的FILE结构对象并设置相关信息:

FILE_ *fopen_(const char *path_name, const char *mode)
{
    int flags = 0;
    int defaultMode=0666;

    if (strcmp(mode, "r") == 0)
    {
        // O_RDONLY -- 只读
        flags |= O_RDONLY;
    }
    else if (strcmp(mode, "w") == 0)
    {
        // O_WRONLY -- 只写,O_CREAT -- 文件不存在时创建文件,O_TRUNC -- 打开文件时先清空文件
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if (strcmp(mode, "a") == 0)
    {
        // O_APPEND -- 追加
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    else
    {
        // ...
    }
    
    int fd = 0;

    // 如果是以只写的方式打开,则默认文件已经存在,直接打开即可
    if (flags & O_RDONLY) 
        fd = open(path_name, flags);
    // 不然则可能需要创建文件,一旦flags中有O_CREAT信息则需要传创建文件时的默认权限
    else 
        fd = open(path_name, flags, defaultMode);
    
    // 打开文件失败,设置错误信息并返回NULL,这也是为什么打开文件时会返回NULL的原因
    if (fd < 0)
    {
        const char *err = strerror(errno);
        write(2, err, strlen(err));
        return NULL;
    }
    
    // 打开文件成功,创建相关的的FILE结构对象
    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    // 设置FILE对象的字段信息
    fp->flags = SYNC_LINE; 		 // 默认设置成为行刷新
    fp->fileno = fd;	   		 // 设置文件描述符
    fp->cap = SIZE;		   		 // 设置缓冲区容量
    fp->size = 0;		  		 // 初始未进行写入
    memset(fp->buffer, 0 , SIZE);// 给缓冲区申请空间

    return fp; 	// 将fp返回
}

对于fclose_,我们也是直接调用系统调用接口close,只不过在关闭文件之前先刷新一下文件的缓冲区。

注意,C语言中的close函数只负责关闭文件,也就是断掉文件和文件描述符直接的连接关系。

FILE结构的生命周期由标准库管理。当程序终止时,所有仍然打开的文件都会被自动关闭,相应的资源和FILE结构会被释放。这也包括在main函数返回之前打开的文件。所以我们不需要显式地释放FILE结构。

void fclose_(FILE_ *fp)
{
    fflush_(fp);
    close(fp->fileno);
}

对于fwrite_,很简单,直接把要写入的数据拷贝到文件的缓冲区即可,此时缓冲区中可能已经有部分数据,所以写入的时候要注意细节。然后需要根据当前文件的刷新策略判断一下是否需要刷新。而数据从缓冲区刷新出去的本质就是调用write接口将数据从缓冲区中写入到文件对应的文件描述符中,然后将缓冲区的size置零进行惰性删除缓冲区内的数据:

void fwrite_(const void *ptr, int num, FILE_ *fp)
{
    // 写入到缓冲区中
    memcpy(fp->buffer + fp->size, ptr, num); //这里我们不考虑缓冲区溢出的问题
    fp->size += num;

    // 无缓冲,数据读进来之后立即刷新
    if (fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    
    // 全缓冲
    else if (fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->cap)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    
    // 行缓冲
    else if (fp->flags & SYNC_LINE)
    {
        // 这里只是为了简单地理解原理,不考虑诸如"abcd\nef"这类较为复杂的场景
        if(fp->buffer[fp->size-1] == '\n') 
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
}

最后就是fflush_,直接调用系统调用接口write将缓冲区的数据写到文件中,然后将缓冲区置空就好了。不过此时还有一个问题,我们用write将数据写入到文件,实际中间还有OS的内核缓冲区,可能不会直接刷新到硬盘中的文件,所以可以通过系统调用接口fsync强制要求OS将数据刷新到外设:

void fflush_(FILE_ *fp)
{
    if(fp->size > 0) 
        write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno); // 强制要求OS将数据刷新到外设
    fp->size = 0;      // 将缓冲区置空,无需对缓冲区原有的数据进行操作
}

最后我们整合一下,把所有相关头文件包含和声明放到头文件stdio_.h中:

#pragma once

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

#define SIZE 1024
#define SYNC_NOW    1 << 0  //无缓冲
#define SYNC_LINE   1 << 1  //行缓冲
#define SYNC_FULL   1 << 2  //全缓冲

typedef struct FILE_{
    int flags;        //刷新策略
    int fileno;       //文件描述符
    int cap;          //buffer的总容量
    int size;         //buffer当前的使用量
    char buffer[SIZE];//缓冲区,这里大小是写死的,实际缓冲区的大小是可变的
}FILE_;


FILE_ *fopen_(const char *path_name, const char *mode); 
// path_name是要打开文件的路径
// mode是打开文件时的方式,是读、写还是追加

void fwrite_(const void *ptr, int num, FILE_ *fp);
// ptr是要写入的数据的地址
// num是要写入的数据的字节数
// fp是要向哪个文件中写入

void fclose_(FILE_ * fp);
// fp是要关闭的文件

void fflush_(FILE_ *fp);
// 立即刷新fp的缓冲区

相关方法的实现整合在stdio_.c文件中:

#include "myStdio.h"


FILE_ *fopen_(const char *path_name, const char *mode)
{
    int flags = 0;
    int defaultMode=0666;

    if (strcmp(mode, "r") == 0)
    {
        // O_RDONLY -- 只读
        flags |= O_RDONLY;
    }
    else if (strcmp(mode, "w") == 0)
    {
        // O_WRONLY -- 只写,O_CREAT -- 文件不存在时创建文件,O_TRUNC -- 打开文件时先清空文件
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if (strcmp(mode, "a") == 0)
    {
        // O_APPEND -- 追加
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    else
    {
        // ...
    }
    
    int fd = 0;

    // 如果是以只写的方式打开,则默认文件已经存在,直接打开即可
    if (flags & O_RDONLY) 
        fd = open(path_name, flags);
    // 不然则可能需要创建文件,一旦flags中有O_CREAT信息则需要传创建文件时的默认权限
    else 
        fd = open(path_name, flags, defaultMode);
    
    // 打开文件失败,设置错误信息并返回NULL,这也是为什么打开文件时会返回NULL的原因
    if (fd < 0)
    {
        const char *err = strerror(errno);
        write(2, err, strlen(err));
        return NULL;
    }
    
    // 打开文件成功,创建相关的的FILE结构对象
    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    // 设置FILE对象的字段信息
    fp->flags = SYNC_LINE; 		 // 默认设置成为行刷新
    fp->fileno = fd;	   		 // 设置文件描述符
    fp->cap = SIZE;		   		 // 设置缓冲区容量
    fp->size = 0;		  		 // 初始未进行写入
    memset(fp->buffer, 0 , SIZE);// 给缓冲区申请空间

    return fp; 	// 将fp返回
}


void fclose_(FILE_ *fp)
{
    fflush_(fp);
    close(fp->fileno);
}


void fwrite_(const void *ptr, int num, FILE_ *fp)
{
    // 写入到缓冲区中
    memcpy(fp->buffer + fp->size, ptr, num); //这里我们不考虑缓冲区溢出的问题
    fp->size += num;

    // 无缓冲,数据读进来之后立即刷新
    if (fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    
    // 全缓冲
    else if (fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->cap)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    
    // 行缓冲
    else if (fp->flags & SYNC_LINE)
    {
        // 这里只是为了简单地理解原理,不考虑诸如"abcd\nef"这类较为复杂的场景
        if(fp->buffer[fp->size-1] == '\n') 
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
}


void fflush_(FILE_ *fp)
{
    if(fp->size > 0) 
        write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno); // 强制要求OS将数据刷新到外设
    fp->size = 0;      // 将缓冲区置空,无需对缓冲区原有的数据进行操作
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ae 效果:CC Image Wipe

过渡/CC Image Wipe Transition/CC Image Wipe CC Image Wipe&#xff08;CC 图像擦除&#xff09;效果的主要用途是通过使用图像的某个属性&#xff08;如红通道、绿通道、蓝通道、Alpha 通道、亮度、明度、色相或饱和度&#xff09;来创建一个独特的过渡效果&#xff0c;可以…

【算法与数据结构】112、LeetCode路径总和

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;本题通过计算根节点到叶子节点路径上节点的值之和&#xff0c;然后再对比目标值。利用文章【算法和数据…

浅析Redis(1)

一.Redis的含义 Redis可以用来作数据库&#xff0c;缓存&#xff0c;流引擎&#xff0c;消息队列。redis只有在分布式系统中才能充分的发挥作用&#xff0c;如果是单机程序&#xff0c;直接通过变量来存储数据是更优的选择。那我们知道进程之间是有隔离性的&#xff0c;那么re…

Android屏幕适配(5) — 最小宽度smallWidth适配

概述 最小宽度smallWidth适配实现屏幕适配方案 详细 前言 在之前的文章中&#xff0c;我们讲到了Android屏幕适配的一些知识&#xff0c;大家感兴趣的话可参考Android屏幕适配(1) — 概念解释Android屏幕适配(2) — drawable与mipmapAndroid屏幕适配(3) — 资源文件夹命名与…

音频母带制作::AAMS V4.0 Crack

自动音频母带制作简介。 使用 AAMS V4 让您的音乐听起来很美妙&#xff01; 作为从事音乐工作的音乐家&#xff0c;您在向公众发布材料时需要尽可能最好的声音&#xff0c;而为所有音频扬声器系统提供良好的商业声音是一项困难且耗时的任务。AI掌握的力量&#xff01; 掌控您…

网易新财报:游戏稳、有道进、云音乐正爬坡

今年以来&#xff0c;AI大模型的火热程度屡屡攀升&#xff0c;越来越多的企业都加入到了AI大模型的赛场中&#xff0c;纷纷下场布局。而在众多参与者中&#xff0c;互联网企业的身影更是频频浮现&#xff0c;比如&#xff0c;百度、阿里巴巴、腾讯等等。值得一提的是&#xff0…

mysql 存储引擎系列 (二) Innodb 存储引擎 和 myisam 存储引擎

InnoDB 引擎 支持外键InnoDB是MySQL默认事务型引擎&#xff0c;它被设计用来处理大量的短期事务。可以确保事务的完整提交&#xff08;commit&#xff09;和回滚&#xff08;Rollback&#xff09;增加和查询&#xff0c;且数据量少的&#xff0c;MyIsam 高效&#xff0c; 除了…

ps beta 2.5的妙用

1、https://pan.baidu.com/s/1CCw6RGlzEJ7TPWou8pPADQ?pwd2023 2、下载新便携版。 3、解压到c:\myapp文件夹下。 4、运行。 5、登录us账号。 6、使用智能移除。 效果如下&#xff1a; 使用滤镜。 先将C:\myApp\&#xff08;新便携版&#xff09;Adobe Photoshop (25.0.0 m22…

香橙派OrangePi zero H2+ 驱动移远4G/5G模块

目录 1 安装系统和内核文件&#xff1a; 1.1 下载镜像 1.2 内核头安装 1.2.1 下载内核 1.2.2 将内核头文件导入开发板中 1.2.3 安装内核头 2 安装依赖工具&#xff1a; 2.1 Installing Required Host Utilities 3 驱动步骤&#xff1a; 3.1 下载模块驱动文件…

微信小程序云开发案列

基础知识&#xff1a; async 微信小程序中 methods里面使用async 微信小程序中可以在methods里面使用async关键字来定义异步函数。例如&#xff1a; Page({data: {name: Tom,},async onLoad() {const res await wx.request({url: https://api.example.com/users,method: GE…

6、css学习6(表格)

1、指定CSS表格边框&#xff0c;使用border属性。 2、表格双边框是因为th/td有各自独立的边框。 3、boder-collapse设置表格边框是否被折叠成一个单一的边框。 4、width和height属性定义表格的宽度和高度。 5、text-align属性设置水平对齐方式。 6、vertic-align属性设置垂…

循环神经网络(RNN) | 项目还不成熟 |还在初级阶段

一&#xff0c;定义 循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;是一种深度学习神经网络架构&#xff0c;专门设计用于处理序列数据&#xff0c;如时间序列数据、自然语言文本等&#xff08;一般用来解决序列问题&#xff09;。 因为它们具…

YOLOv5、YOLOv8改进:CoordAtt注意力机制

目录 1.简介 2.Coordinate Attention 2.2 Coordinate Attention生成 3.YOLOv5改进 3.1common.py构建CoordAtt模块。 3.2 yolo.py中注册CoordAtt模块 3.3 yaml文件配置 1.简介 最近对移动网络设计的研究已经证明了通道注意力&#xff08;例如&#xff0c;Squeeze-an…

验证go循环删除slice,map的操作和map delete操作不会释放底层内存的问题

目录 切片 for 循环删除切片元素其他循环中删除slice元素的方法方法1方法2&#xff08;推荐&#xff09;方法3 官方提供的方法结论 切片 for 循环删除map元素goalng map delete操作不会释放底层内存 切片 for 循环删除切片元素 在 Go 语言中&#xff0c;使用 for 循环删除切片…

BookStack开源免费知识库docker-compose部署

BookStack&#xff08;书栈&#xff09;是一个功能强大且易于使用的开源知识管理平台&#xff0c;适用于个人、团队或企业的文档协作和知识共享。 一、BookStack特点 简单易用&#xff1a;BookStack提供了一个直观的用户界面&#xff0c;使用户能够轻松创建、编辑和组织文档多…

SpringBoot整合 redis key (过期、新增、修改)的三种方式,看这篇就够了

文章目录 原理关于 *notify-keyspace-events*关于redis的消息主题&#xff08;Topic&#xff09;重写监听容器注册自定义解析常见整合问题鸣谢 文章主要描述了Springboot整合key变化的三种方式&#xff0c;同时列出了一些整合坑点与概念 原理 SpringBoot整合Redis key变化的原…

二叉树的非递归遍历实现(三种)

1、先序遍历 先序遍历使用了栈的结构&#xff0c;先压入根节点&#xff0c;然后依次将其右子节点和左字节点压入。然后就可以实现“头左右”的遍历顺序 /*** 先序遍历*/public static void pre_order(TreeNode treeNode){if (treeNode null){return;}Stack<TreeNode> …

PAT 1136 A Delayed Palindrome

个人学习记录&#xff0c;代码难免不尽人意 A B C where A is the original number, B is the reversed A, and C is their sum. A starts being the input number, and this process ends until C becomes a palindromic number – in this case we print in the last line …

Graylog 更改显示的时区(Display timezone)

每个 Graylog 用户都可以配置他们的显示时区。 这会更改用于查看日志消息的时区&#xff0c;但不会更改日志消息的原始时区。 默认情况下&#xff0c;Graylog 显示 UTC 格式的所有时间戳&#xff08;也就是 0:00&#xff09;。就像是下面这样 非Admin账户要更改时区&#xff1…

防御网络攻击风险的4个步骤

如今&#xff0c;人们正在做大量工作来保护 IT 系统免受网络犯罪的侵害。令人担忧的是&#xff0c;对于运营技术系统来说&#xff0c;情况却并非如此&#xff0c;运营技术系统用于运行从工厂到石油管道再到发电厂的所有业务。 组织应该强化其网络安全策略&#xff0c;因为针对…