【Netty】ChannelPipeline源码分析(五)

news2024/11/23 7:49:30

文章目录

  • 前言
  • 一、ChannelPipeline 接口
    • 1.1 创建 ChannelPipeline
    • 1.2 ChannelPipeline 事件传输机制
      • 1.2.1 处理出站事件
      • 1.2.2 处理入站事件
  • 二、ChannelPipeline 中的 ChannelHandler
  • 三、ChannelHandlerContext 接口
    • 3.1 ChannelHandlerContext 与其他组件的关系
    • 3.2 跳过某些 ChannelHandler
  • 总结

前言

我们在前面的文章中也对ChannelPipeline接口做了初步的介绍。

  • Netty 概述(一)
  • Netty 架构设计(二)
  • Netty Channel 概述(三)
  • Netty ChannelHandler(四)

一、ChannelPipeline 接口

ChannelPipeline接口采用了责任链设计模式,底层采用双向链表的数据结构,将链上的各个处理器串联起来。客户端每一个请求的到来,ChannelPipeline中所有的处理器都有机会处理它。

每一个新创建的Channel都将会被分配一个新的ChannelPipeline。这项关联是永久性的;Channel既不能附加另一个ChannelPipeline,也不能分离其当前的。

1.1 创建 ChannelPipeline

ChannelPipeline数据管道是与Channel管道绑定的,一个Channel通道对应一个ChannelPipeline,ChannelPipeline是在Channel初始化时被创建。

观察下面这个实例:

public void run() throws Exception {
    EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap(); // (2)
        b.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class) // (3)
            .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                @Override
                public void initChannel(SocketChannel ch) throws Exception {

                    // 添加ChannelHandler到ChannelPipeline
                    ch.pipeline().addLast(new DiscardServerHandler());
                }
            })
            .option(ChannelOption.SO_BACKLOG, 128)          // (5)
            .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

        // 绑定端口,开始接收进来的连接
        ChannelFuture f = b.bind(port).sync(); // (7)

        System.out.println("DiscardServer已启动,端口:" + port);

        // 等待服务器  socket 关闭 。
        // 在这个例子中,这不会发生,但你可以优雅地关闭你的服务器。
        f.channel().closeFuture().sync();
    } finally {
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }
}

从上述代码中可以看到,当ServerBootstrap初始化后,直接就可以获取到SocketChannel上的ChannelPipeline,而无需手动实例化,因为 Netty 会为每个Channel连接创建一个ChannelPipeline。

Channel的大部分子类都继承了AbstractChannel,在创建实例时也会调用AbstractChannel构造器。在AbstractChannel构造器中会创建ChannelPipeline管道实例,核心代码如下:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    this.id = this.newId();
    this.unsafe = this.newUnsafe();
    this.pipeline = this.newChannelPipeline();
}

protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}

从上述代码中可以看出,在创建Channel时,会由Channel创建DefaultChannelPipeline类的实例。DefaultChannelPipeline是ChannelPipeline的默认实现。

pipeline是AbstractChannel的属性,内部维护着一个以AbstractChannelHandlerContext为节点的双向链表,创建的head和tail节点分别指向链表头尾,源码如下:

public class DefaultChannelPipeline implements ChannelPipeline {   	

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = (Channel)ObjectUtil.checkNotNull(channel, "channel");
        this.succeededFuture = new SucceededChannelFuture(channel, (EventExecutor)null);
        this.voidPromise = new VoidChannelPromise(channel, true);
        this.tail = new DefaultChannelPipeline.TailContext(this);
        this.head = new DefaultChannelPipeline.HeadContext(this);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }


    ...
    
    final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
        TailContext(DefaultChannelPipeline pipeline) {
            super(pipeline, (EventExecutor)null, DefaultChannelPipeline.TAIL_NAME, DefaultChannelPipeline.TailContext.class);
            this.setAddComplete();
        }

    ...
    }
    
      final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler {
        private final Unsafe unsafe;

        HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, (EventExecutor)null, DefaultChannelPipeline.HEAD_NAME, DefaultChannelPipeline.HeadContext.class);
            this.unsafe = pipeline.channel().unsafe();
            this.setAddComplete();
        }
          
     	...
          
      }
    
     ...
}

从上述源码可以看到,TailContext和HeadContext都继承了AbstractChannelHandlerContext,并实现了ChannelHandler接口。AbstractChannelHandlerContext内部维护着next、prev链表指针和入站、出站节点方向等。其中TailContext实现了ChannelInboundHandler,HeadContext实现了ChannelOutboundHandler和ChannelInboundHandler。

1.2 ChannelPipeline 事件传输机制

通过ChannelPipeline的addFirst()方法来添加ChannelHandler,并为这个ChannelHandler创建一个对应的DefaultChannelHandlerContext实例。

public class DefaultChannelPipeline implements ChannelPipeline {  
    //...
    
	public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) {
        AbstractChannelHandlerContext newCtx;
        synchronized(this) {
            checkMultiplicity(handler);
            name = this.filterName(name, handler);
            newCtx = this.newContext(group, name, handler);
            this.addFirst0(newCtx);
            if (!this.registered) {
                newCtx.setAddPending();
                this.callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                this.callHandlerAddedInEventLoop(newCtx, executor);
                return this;
            }
        }

        this.callHandlerAdded0(newCtx);
        return this;
    }
    
    //...
    

    private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
        return new DefaultChannelHandlerContext(this, this.childExecutor(group), name, handler);
    }
    
    //...

}

1.2.1 处理出站事件

当处理出站事件时,channelRead()方法的示例如下:

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		System.out.println(ctx.channel().remoteAddress() + " -> Server :" + msg);
		
		// 写消息到管道
		ctx.write(msg);// 写消息
	}
	
	//...
}

上述代码中的write()方法会触发一个出站事件,该方法会调用DefaultChannelPipeline上的write()方法。

public final ChannelFuture write(Object msg) {
    return this.tail.write(msg);
}

从上述源码可以看到,调用的是DefaultChannelPipeline上尾部节点(tail)的write方法。
上述方法最终会调用到DefaultChannelHandlerContext的write()方法。

private void write(Object msg, boolean flush, ChannelPromise promise) {
    ObjectUtil.checkNotNull(msg, "msg");

    try {
        if (this.isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            return;
        }
    } catch (RuntimeException var8) {
        ReferenceCountUtil.release(msg);
        throw var8;
    }

    AbstractChannelHandlerContext next = this.findContextOutbound(flush ? 98304 : '耀');
    Object m = this.pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }
    } else {
        AbstractChannelHandlerContext.WriteTask task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise, flush);
        if (!safeExecute(executor, task, promise, m, !flush)) {
            task.cancel();
        }
    }

}

上述的write()方法会查找下一个出站的节点,也就是当前ChannelHandler后的一个出站类型的ChannelHandler,并调用下一个节点的invokeWrite()方法。

void invokeWrite(Object msg, ChannelPromise promise) {
    if (this.invokeHandler()) {
        this.invokeWrite0(msg, promise);
    } else {
        this.write(msg, promise);
    }

}

接着调用invokeWrite0()方法,该方法最终调用ChannelOutboundHandler的write方法。

private void invokeWrite0(Object msg, ChannelPromise promise) {
        try {
            ((ChannelOutboundHandler)this.handler()).write(this, msg, promise);
        } catch (Throwable var4) {
            notifyOutboundHandlerException(var4, promise);
        }

    }

至此,处理完成了第一个节点的处理,开始执行下一个节点并不断循环。
所以,处理出站事件时,数据传输的方向是从尾部节点tail到头部节点head。

1.2.2 处理入站事件

入站事件处理的起点是触发ChannelPipeline fire方法,例如fireChannelActive()方法的示例如下:

public class DefaultChannelPipeline implements ChannelPipeline {   	  
    //...
    
    public final ChannelPipeline fireChannelActive() {
            AbstractChannelHandlerContext.invokeChannelActive(this.head);
            return this;
        }
    //...
}

从上述源码可以看到,处理的节点是头部节点head。AbstractChannelHandlerContext.invokeChannelActive方法定义如下:

static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
    } else {
        executor.execute(new Runnable() {
            public void run() {
                next.invokeChannelActive();
            }
        });
    }

}

该方法最终调用ChannelInboundHandler的channelActive方法。

private void invokeChannelActive() {
    if (this.invokeHandler()) {
        try {
            ((ChannelInboundHandler)this.handler()).channelActive(this);
        } catch (Throwable var2) {
            this.invokeExceptionCaught(var2);
        }
    } else {
        this.fireChannelActive();
    }

}

至此完成了第一个节点的处理,开始执行下一个节点的不断循环。
所以,处理入站事件时,数据传输的方向是从头部节点head到尾部节点tail。

二、ChannelPipeline 中的 ChannelHandler

从上述的ChannelPipeline 接口源码可以看出,ChannelPipeline 是通过addXxx或者removeXxx方法来将ChannelHandler动态的添加到ChannelPipeline中,或者从ChannelPipeline移除ChannelHandler的。那么ChannelPipeline是如何保障并发访问时的安全呢?

以addLast方法为例,DefaultChannelPipeline的源码如下:

public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    AbstractChannelHandlerContext newCtx;
    //synchronized 保障线程安全
    synchronized(this) {
        checkMultiplicity(handler);
        newCtx = this.newContext(group, this.filterName(name, handler), handler);
        this.addLast0(newCtx);
        if (!this.registered) {
            newCtx.setAddPending();
            this.callHandlerCallbackLater(newCtx, true);
            return this;
        }

        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            this.callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }

    this.callHandlerAdded0(newCtx);
    return this;
}

从上述源码可以看到,使用synchronized关键字保障了线程的安全访问。其他方法的实现方式也是类似。

三、ChannelHandlerContext 接口

ChannelHandlerContext 接口是联系ChannelHandler和ChannelPipeline 之间的纽带。

每当有ChannelHandler添加到ChannelPipeline 中时,都会创建ChannelHandlerContext 。
ChannelHandlerContext 的主要功能是管理它所关联的ChannelHandler和在同一个ChannelPipeline 中的其他ChannelHandler之间的交互。

例如,ChannelHandlerContext 可以通知ChannelPipeline 中的下一个ChannelHandler开始执行及动态修改其所属的ChannelPipeline 。

ChannelHandlerContext 中包含了许多方法,其中一些方法也出现在Channel和ChannelPipeline 中。如果通过Channel或ChannelPipeline 的实例来调用这些方法,它们就会在整个ChannelPipeline 中传播。相比之下,一样的方法在ChannelHandlerContext 的实例上调用,就只会从当前ChannelHandler开始并传播到相关管道中的下一个有处理事件能力的ChannelHandler中。因此ChannelHandlerContext 所包含的事件流比其他类中同样的方法都要短,利用这一点可以尽可能提高性能。

3.1 ChannelHandlerContext 与其他组件的关系

下图展示了ChannelPipeline 、Channel、ChannelHandler和ChannelHandlerContext 之间的关系做了如下说明:
在这里插入图片描述

  1. Channel被绑定到ChannelPipeline 上。
  2. 和Channel绑定的ChannelPipeline 包含了所有的ChannelHandler。
  3. ChannelHandler。
  4. 当添加ChannelHandler到ChannelPipeline 时,ChannelHandlerContext 被创建。

3.2 跳过某些 ChannelHandler

下面的代码,展示了从ChannelHandlerContext 获取到Channel的引用,并通过调用Channel上的write()方法来触发一个写事件到流中。

ChannelHandlerContext ctx = context;
Channel channel = ctx.channel(); //获取ChannelHandlerContext上的Channel
channel.write(msg);

以下代码展示了从ChannelHandlerContext 获取到ChannelPipeline 。

ChannelHandlerContext ctx = context;
ChannelPipeline pipeline = ctx.pipeline(); //获取ChannelHandlerContext上的ChannelPipeline 
pipeline.write(msg);

上述的两个示例,事件流是一样的。虽然被调用的Channel和ChannelPipeline 上的write()方法将一直传播事件通过整个ChannelPipeline ,但是在ChannelHandler的级别上,事件从一个ChannelHandler到下一个ChannelHandler的移动是由ChannelHandlerContext 上的调用完成的。

下图展示了Channel或者ChannelPipeline 进行的事件传播机制。
在这里插入图片描述

在上图中可以看出:

  1. 事件传递给ChannelPipeline 的第一个ChannelHandler;
  2. ChannelHandler通过关联的ChannelHandlerContext 传递事件给ChannelPipeline 中的下一个ChannelHandler。
  3. ChannelHandler通过关联的ChannelHandlerContext 传递事件给ChannelPipeline 中的下一个ChannelHandler。

从上面的流程可以看出,如果通过Channel或ChannelPipeline 的实例来调用这些方法,它们肯定会在整个ChannelPipeline 中传播。

那么是否可以跳过某些处理器呢?答案是肯定的。

通过减少ChannelHandler不感兴趣的事件的传递减少开销,并排除掉特定的对此事件感兴趣的处理器的处理以提升性能。想要实现从一个特定的ChannelHandler开始处理,必须引用与此ChannelHandler的前一个ChannelHandler关联的ChannelHandlerContext 。这个ChannelHandlerContext 将会调用与自身关联的ChannelHandler的下一个ChannelHandler,代码如下:

ChannelHandlerContext ctx = context;
ctx.write(msg);

直接调用ChannelHandlerContext 的write()方法,将会把缓冲区发送到下一个ChannelHandler。

如下图,消息会将从下一个ChannelHandler开始流过ChannelPipeline ,绕过所有在它之前的ChannelHandler。
在这里插入图片描述

  1. 执行ChannelHandlerContext 方法调用。
  2. 事件发送到了下一个ChannelHandler。
  3. 经过最后一个ChannelHandler后,事件从ChannelPipeline 中移除。

当调用某个特定的ChannelHandler操作时,它尤为有用。
例如:

public class EchoServerHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		System.out.println(ctx.channel().remoteAddress() + " -> Server :" + msg);

        // 写消息到管道
		ctx.write(msg);// 写消息
		ctx.flush(); // 冲刷消息
		
		// 上面两个方法等同于 ctx.writeAndFlush(msg);
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {

		// 当出现异常就关闭连接
		cause.printStackTrace();
		ctx.close();
	}
}

总结

以上就是关于ChannelPipeline 的源码分析,相信认真看完了,你就明白ChannelPipeline 、Channel、ChannelHandler和ChannelHandlerContext 之间的关系。下节我们继续来剖析 Netty 的源码。

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

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

相关文章

tcp拥塞控制

序列号: 在TCP连接中传送的字节流中的每一个字节都按顺序编号。该字段表明发送数据的第一个字节的序号 确认号&#xff1a;希望收到对方下一个报文的第一个字节的序号 窗口&#xff1a;用于通知发送端&#xff0c;接收端可以接收的空间大小 TCP的流量控制是使用滑动窗口机制&…

C4d Octane渲染器内存满、卡顿、崩溃、缓慢、updating解决办法

最近碰到Octane渲染动画序列&#xff0c;总是会渲染一段时间后卡在某一张图片上&#xff0c;图片查看器左下角一直显示updating。 偶然发现在C4D界面点击octane工具栏的设置&#xff0c;它又会开始渲染&#xff0c;但渲染一些序列帧后又会卡在一张图上显示updating 点击octane工…

MyBatis-Plus01_简介、入门案例、BaseMapper与IService中的CRUD以及常用的注解

目录 ①. MyBatis-plus简介 ②. MyBatis-plus入门案例 ③. BaseMapper中的CRUD ④. 通用Service的CRUD ⑤. MyBatis-plus中常用注解TableName&#xff1a; ⑥. 常用注解TableId ⑦. 雪花算法 ⑧常用注解TableField ⑨. 常用注解TableLogic——逻辑删除专用注解 ①. M…

chatgpt赋能python:PythonUDS:让你的汽车掌握更多技能

Python UDS&#xff1a;让你的汽车掌握更多技能 UDS&#xff08;Unified Diagnostic Services&#xff09;是一种汽车电子控制单元&#xff08;ECU&#xff09;通信协议&#xff0c;用于车辆的诊断和测试。Python UDS是用Python编程语言实现的UDS客户端和服务器实现&#xff0…

【stable diffusion保姆级教程,左手ChatGPT之剑,右手stablediffusion之矛】

一、前言 哈喽&#xff0c;大家好&#xff0c;我是Tian-Feng&#xff0c;前面写过两篇文章&#xff0c;但是细节没认真写&#xff0c;除了介绍一些参数意思&#xff0c;和推荐模型插件&#xff0c;有一定基础的小伙伴应该是挺有用的&#xff0c;但如果是小白&#xff0c;可能还…

完全二叉树——堆的概念及实现

前言 堆(heap)&#xff1a;是堆内存的简称&#xff0c;堆是动态分配内存&#xff0c;内存大小不固定&#xff0c;也不会自动释放&#xff0c;堆——数据结构是一种无序的树状结构&#xff0c;同时它还满足key-value键值对的存储方式。 1. 堆的概念及结构 如果有一个关键码的…

BFC与IFC

概念 块级元素在BFC布局&#xff08;块级格式化上下文&#xff09; 行内级元素在IFC布局&#xff08;行内级格式化上下文&#xff09; BFC 形成BFC的情况 BFC规则 在BFC中box在垂直方向排列在同一个BFC中&#xff0c;相邻box垂直方向外边距塌陷在BFC中box左边缘紧贴包含块的…

Python数据结构与算法篇(十五)-- 二叉树的遍历:深度优先搜索与广度优先搜索

本篇开始总结二叉树的常用解题技巧&#xff0c;二叉树的顺序遍历和层序遍历刚好对应深度优先搜索和广度优先搜索。 1 顺序遍历 题目列表 144. 前序遍历145. 二叉树的后序遍历 94. 二叉树的中序遍历 144. 二叉树的前序遍历 给你二叉树的根节点 root &#xff0c;返回它…

程序员开发之“留一手“

很多乙方公司为了顺利获得项目的尾款&#xff0c;或者有些项目的封装整合的逻辑比较多&#xff0c;通常会把项目的业务逻辑代码及架构进行打包成线上NuGet包。 一、 NuGet包 其实就是线上的.dll文件 &#xff0c;在本地编译后上传是NuGet 1、首先注册NuGet 2、记住API Key …

chatgpt赋能python:Python*a:提高代码效率的利器

Python *a&#xff1a;提高代码效率的利器 Python是一种高层次、通用性编程语言。Python的简洁语法和宽松语义&#xff0c;让它成为了软件开发、数据分析、科学计算等领域的首选语言之一。Python也因其易学、易读、易部署的特点而被全球越来越多的开发者所喜爱。在这篇文章中&…

Redis事务及网络处理

一 Redis事务 redis开启事务后&#xff0c;会把接下来的所有命令缓存到一个单独的队列中&#xff0c;在提交事务时&#xff0c;使这些命令不可被分割的一起执行完成。 如果使用了watch命令监视某一个key&#xff0c;如果在开启事务之后&#xff0c;提交事务之前&#xff0c;有…

超级牛散也踩雷!这A股宣布大消息

公司被债权人申请重整一事被法院正式立案7个多月后&#xff0c;5月24日&#xff0c;*ST搜特收到了法院的终结预重整程序通知书和不予受理重整申请裁定书。 消息曝出后&#xff0c;*ST搜特股吧则瞬间炸锅&#xff0c;投资者纷纷留言“完了”、“没盼头了”、“最后的希望终究还…

浅谈IAM——OAuth2.0攻击方法总结

一、OAuth协议介绍 OAuth是一种标准授权协议&#xff0c;它允许用户在不需要向第三方网站或应用提供密码的情况下向第三方网站或应用授予对存储于其他网站或应用上的信息的委托访问权限。OAuth通过访问令牌来实现这一功能。 1.发展历史 OAuth协议始于2006年Twitter公司OpenI…

车载以太网 - SomeIP - 协议用例 - BehaviorBasic

目录 Service Discovery Communication Behavior 1、验证DUT启动后的重复报文阶段,2帧offer报文之间的时间间隔为上次的2倍<

【模型预测】A-4D战斗机姿态控制的模型预测控制方法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Java中的类加载机制

Java中的类加载机制 类的生命周期 ​ 一个类型从被加载到虚拟机内存中开始&#xff0c;到卸载出内存为止&#xff0c;它的整个生命周期将会经历加载&#xff08;Loading&#xff09;、验证&#xff08;Verification&#xff09;、准备&#xff08;Preparation&#xff09;、解…

C++ thread编程(Linux系统为例)—thread成员函数与thread的创建方法

c 11 之后有了标准的线程库&#xff1a;std::thread。 参考thread库的使用 成员函数 构造函数 thread的构造函数有下面四个重载 默认构造函数 thread() noexcept初始化构造函数 template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&a…

Linux命令(21)之usermod

Linux命令之usermod 1.usermod介绍 usermod命令用来更改/etc/passwd或/etc/shadow文件下用户属性&#xff0c;包括但不限于shell类型、用户id&#xff0c;用户gid、家目录、锁定及解锁用户等等。 2.usermod用法 usermod [参数] [用户名] usermod常用参数 参数说明-u修改UID…

淘宝/天猫商品评论数据采集

淘宝商品评论API接口是指允许开发者通过API接口获取淘宝商品的评价信息的一种技术手段。通过使用这个接口&#xff0c;开发者可以快速获取淘宝商品的评价信息&#xff0c;以实现自己的商业用途。 淘宝商品评论API接口需要开发者提供相应的数据访问权限&#xff0c;包括授权和Ap…

Linux命令(22)之chage

Linux命令之chage 1.chage介绍 usermod命令用来更改linux用户密码到期信息&#xff0c;包括密码修改间隔最短、最长日期、密码失效时间等等。 2.chage用法 chage常用参数 参数说明-m密码可更改的最小天数&#xff0c;为0表示可以随时更改-M密码有效期最大天数-W密码到期前提…