Netty应用(四) 之 Reactor模型 零拷贝

目录

6.Reactor模型

6.1 单线程Reactor

6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制)

7.扩展与补充

8.Reactor模型的实现

8.1 多线程Reactor模型的实现(一个Boss线程,一个Worker线程)

8.2 多线程Reactor模型的实现(一个Boss线程,多个Worker线程)

9.零拷贝

10.硬件驱动程序与软件驱动程序(数据库驱动程序)的关系

11.linux内核与centos,redhat等这些产品有啥区别和关系?

6.Reactor模型

6.1 单线程Reactor

6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制)

但是单线程版的Reactor,一定存在性能瓶颈的!你想想一个线程做了所有的连接,读,写的操作,肯定存在极大的性能瓶颈。

所以后续引入了主从多线程Reactor模式:

1.Boss主线程负责监听并且处理客户端的连接事件。它具有一个独立的Selector监控器,所有的客户端都会注册存储到该Selector上,这样才能完成监听连接事件的操作。

2.当Boss主线程监听并处理完客户端的连接事件后,它会把这个客户端交给Worker从线程进行处理,从线程肯定会先把该客户端像主线程一样先注册存储到当前从线程所对应的Selector上。从线程会进行监听与处理该客户端后续可能会进行的读或写的事件。从线程可能会有多个,每一个从线程都具有一个独立的Selector。

补充:

1.Boss主线程与Worker从线程们之间一定是建立了联系的,这样主线程才能把客户端交给从线程。每一个线程都具有一个独立的Selector

2.分配多少个线程给Boss或Worker?分配一个线程给Boss,剩余(Cpu核数-1)个线程分配给Worker从线程。

先说为什么一共分配CPU核数的线程?为了避免频繁的线程上下文切换,为了实现真正的并行。后续肯定还可以引入线程池的优化

为什么Worker线程分配的多?因为客户端连接就一次,后续Boss把客户端甩手给Worker后,客户端的读写事件就不再让Boss去处理了,读写事件可是远比连接事件多的!

如图所示:

7.扩展与补充

扩展:

主从架构与主备架构的区别:

主从架构:

主节点也干活,从节点也干活。只是二者干的活不一样,主节点一般做写操作(主要的工作),从节点一般做次要的工作 如 读操作。缺点:主节点一旦宕机,就废掉了。

主备架构:

主节点会同步数据到另外一共备用主节点,当主节点宕机时,会切换到另外一台时刻同步数据的备用主节点。解决了主从架构的缺点。

主备架构:MySQL的双主架构,Redis的哨兵集群

如何实现主备架构中,主节点同步数据到另外一台备用主节点呢?

使用zookeeper集群这一CP原理架构,遵循CP的是强一致性的架构,即:当zookeeper集群节点出现宕机时,需要重新选举节点作为主节点时,拒绝对外提供服务,此时就保证了强一致性。但是降低了性能,这就是CAP原则,不可以同时满足C A P。

当然,zookeeper可以替换为consul,etcd

反向代理与正向代理:

反向代理时针对于服务器端:

1.做负载均衡

2.URL重写 :面对写入的URL重写

3.动静分离

新的问题:当客户端连接这个服务器集群时,正在连接的服务器宕机了,你怎么做到客户端无感的?客户端socket连接服务器时,需要通过服务器的ip连接,如果当前这台服务器节点宕机,你不是要更新此时连接的ip?这怎么可能做到无感啊,但通过以下技术就是可以做到无感。。。

方法1:VIP虚拟IP

其实都不需要更新ip,在过去传统的开发中,使用的是Virtual IP 虚拟IP。客户端访问的是虚拟IP对应的服务,虚拟IP这个服务再找到具体的节点服务器资源,虚拟IP服务器肯定会涉及到分发,保活,注册服务器节点等,但是这一切对于客户端来说,是无感知的,它是不知道服务器节点的切换的。

这些都可以通过早期的一些技术完成:如lvs,keepavlid。

方法2:IP飘逸

当发现节点服务器坏了,那么偷偷的把这台服务器节点ip给复制给另外一台服务器节点,这就是ip飘逸,这样也做到了客户端无感知!

解决技术:HAproxy

方法3:云原生OPS自动化运维

这些技术实际上都是运维的技术,现如今微服务为什么说是云原生的一个分支,因为云原生主张OPS,自动化运维,就是要取代运维!

抛开上述三种运维方式,仅仅是站在java层面去开发一个程序去实现这一客户端无感:

其实思路很简单,就是做一个redis缓存,缓存的是ip-主机节点的ip映射集合。

然后在client与server端分别开一个agent,agent之间做网络通信,然后agent再与client或server端内部做交互传递数据。

8.Reactor模型的实现

8.1 多线程Reactor模型的实现(一个Boss线程,一个Worker线程)

Reactor模型:多个客户端连接同一服务端。在服务端做了Reactor模型的编程逻辑。

通过一个Boss线程(可以有多个,看具体业务),处理多个NIO的接收连接的操作,也就是ServerSocketChannel的ACCEPT事件

通过一个worker线程(可以有多个,看具体业务),处理读写的操作,也就是SocketChannel的READ或WRITE事件。

重点:你处理的read或write事件一定是前面对应这次连接的boss线程的连接得来的,你不能前后没联系。是一种传递的关系。但是每一个线程,无论是boss还是worker,都具有独立的Selector监管器

  • 第一版本代码
package com.messi.netty_basic_01.Reactor_prac;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class ReactorBossServer2 {

    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    
                    //TODO 调用Worker线程去处理读写事件~~~~
                
                }
            }
        }
    }
}

对于TODO位置,我们要进行调用worker线程去进行处理读写事件。但是具体要开启多少个worker线程呢?是过来一个客户端连接就需要开启一个worker线程去处理该连接对应的读写事件吗?

当然不是,在之前,我们使用一个线程处理所有注册在复用器上的channel对象对应的所有事件,这里我们使用Boss线程处理连接事件。worker线程处理读写事件,但是也没必要一个线程对应一个客户端连接的后续读写事件啊,如果线程过大,内存占用过大,线程之间切换也会更加频繁,性能很低的。

下面暂且把worker线程设为开启一个,那么我们需要把创建Worker的代码设为公共的,而不能每过来一个客户端连接后,就进行创建一份新的Worker。具体见下面代码

  • 第二版代码
package com.messi.netty_basic_01.Reactor_prac;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class ReactorBossServer2 {

    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //放在外面,做一个共享变量
        Worker2 worker2 = new Worker2();

        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);

                    //TODO 调用Worker线程去处理读写事件~~~~
                    worker2.register(socketChannel);

                }
            }
        }
    }
}

  • 第三版代码
package com.messi.netty_basic_01.Reactor_prac;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class ReactorBossServer2 {

    public static void main(String[] args) throws Exception{
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //放在外面,做一个共享变量
        Worker2 worker2 = new Worker2("worker-01");

        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);

                    //TODO 调用Worker线程去处理读写事件~~~~
                    worker2.register(socketChannel);

                }
            }
        }
    }
}
package com.messi.netty_basic_01.Reactor_prac;

import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;

public class Worker2 implements Runnable{

    private String name;

    private Thread thread ;

    private Selector selector;

    private volatile boolean isCreated;

    public Worker2(String name) {
        this.name = name;
    }

    public void register(SocketChannel socketChannel) {
        if (!isCreated) {
            this.thread = new Thread(this,name);
            thread.start();
            selector = Selector.open();
            isCreated = true;
        }
        
    }

    @Override
    public void run() {

    }
}

为什么Worker2线程类构造方法就一个参数name?

对于其他属性,thread,selector的创建,完全是可以在Worker2内部完成的。woker2线程本身就是Boss线程调用的,Boss线程只初始化worker线程的name,这样解耦合,降低代码的侵入性。你想想,并发场景下,在Boss线程创建多个Thread或Selector,这样是不是Boss线程端代码太难看,造成代码的极大污染。

为什么把thread,selector的初始化创建放在register方法中?

其实也是可以放在构造方法中的。

为什么要使用isCreated标记位?

保证thread线程创建和启动只进行一次!使用标志位isCreated的方式在register中进行初始化。使用标记位isCreated一定要记住:isCreated在完成一次初始化后,要置为true!如果不使用isCreated,boss线程在触发连接事件后调用worker的register方法。但此时还是main线程在执行register方法。那么boss线程触发一次客户端连接事件,就会对应创建一个新线程来处理该客户端连接对应后续的读写事件。

补充:

在多线程并发共享isCreated这个变量时,要加上volatile关键字,volatile可以保证当其中一个线程修改isCreated的值的时候,会及时通知告知其他线程,"isCreated的最新值为xxx"。保证了isCreated的可见性,因为标记位通常做if判断,对可见性要求高。

但此时boss为单线程,并且由于是迭代器一个个遍历触发的事件的,所以同一时刻只有一个boss线程调用register方法,所以isCreated并不存在多线程共享的并发安全问题。养成良好习惯,说不准后续boss变为多线程了。。。

worker2.register(socketChannel)接着调用:

为什么出现空指针异常?

  • 第四版代码
package com.messi.netty_basic_01.Reactor_prac;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class ReactorBossServer2 {

    private static final Logger log = LoggerFactory.getLogger(ReactorBossServer2.class);

    public static void main(String[] args) throws Exception{

        log.debug("boss线程开启 >>>");

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //放在外面,做一个共享变量
        Worker2 worker2 = new Worker2("worker-01");

        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);

                    //TODO 调用Worker线程去处理读写事件~~~~
                    log.debug("boss线程即将要调用Worker2类的register方法 >>>");
                    worker2.register(socketChannel);
                    log.debug("boss线程完成调用Worker2类的register方法 >>>");
                }
            }
        }
    }
}
package com.messi.netty_basic_01.Reactor_prac;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

public class Worker2 implements Runnable{

    private static final Logger log = LoggerFactory.getLogger(Worker2.class);
    private String name;

    private Thread thread ;

    private Selector selector;

    private volatile boolean isCreated;

    public Worker2(String name) {
        this.name = name;
    }

    public void register(SocketChannel socketChannel) throws Exception{

        if (!isCreated) {
            selector = Selector.open();
            this.thread = new Thread(this,name);
            thread.start();
            isCreated = true;
        }
        log.debug("boss线程将要执行socketChannel#register方法 >>>");
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    @Override
    public void run() {
        log.debug("开启一个新线程worker-01 >>>");
        while (true) {
            try {
                selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(30);
                        int read = channel.read(buffer);
                        if (read == -1) {
                            log.debug("客户端断开 >>>");
                            key.cancel();
                        } else {
                            buffer.flip();
                            CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
                            log.info("worker-01线程读取的结果为:{}",decode.toString());
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.messi.netty_basic_01.Reactor_prac;

import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class MyClient2 {

    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8000));
        socketChannel.write(Charset.forName("UTF-8").encode("梅西"));
        System.out.println("MyClient2.main");
    }

}

测试:

  • 但是代码在多线程场景下,存在bug:因为多线程环境下,一切皆有可能,如果新开启的线程执行的run方法(selector.select())在register方法之前执行。由于selector阻塞等待,导致register注册不上。最终就一直阻塞等待着,就死掉了。。。。。下面详细分析一下:

补充一点:

为什么创建一个线程后thread.start()不会立即执行?

因为真正创建出一个线程并且切换都是需要一定时间的,最重要的是当前main线程的时间片可能还没使用完,所以CPU是不会轻易切换线程的,但是在多线程环境下一切是皆有可能,可能start后就切换到该新线程了。但是当main线程时间片耗尽后,就一定会切换到新创建的线程的,即调用run方法中的业务逻辑。通常main线程时间片耗尽的原因是因为执行的业务线复杂导致时间片耗尽,这里我们可以模拟一下,Thread.sleep(2000)休眠2s,此时main线程时间片耗尽,然后就完成了线程切换,线程切换到新创建的线程,并且执行run方法!

但就是因为先执行了run方法,后执行了register方法,代码就出现了bug!

出现bug的原因症结就是因为:run方法(selector.select())在register方法之前执行。由于selector阻塞等待,导致register注册不上。但是在多线程环境下,一切皆有可能,所以你无法保证run方法(selector.select())一定在register方法之后执行!说白了,就是因为register方法和run方法(select()方法调用)二者不是在同一线程中,你无法百分百做到控制方法的执行顺序。

演示一下这个bug:

睡眠模拟bug》》》》

断点设为Thread级别,当前线程挂起时只挂自己。如果设为ALL,那么当前线程挂起时,会挂起所有的线程

debug测试:

切换到main线程,往下执行,发现也阻塞了,阻塞在register这句代码:

分析如下:

解决方法1:

搞一个同步锁,把selector.wakeup()和register方法的调用搞一块:

为什么要搞一个同步锁,为何多此一举呢?

搞同步锁为的就是保证wakeup唤醒和register注册之间没有其他情况的发生,不然不可能保证有意外的发生,如下:

但是同步锁依旧存在很大问题,其实问题就是在于:性能低下。所以后续我们模仿Netty做一个队列,把要执行的代码传递给新开启的线程中,让selector.select()和register方法在同一个线程中执行调用!!并且加一个wakeup做兜底操作,就完美的解决了这个问题。其实就是为了保证在selector.select阻塞selector之前完成register注册READ事件的操作,如果selector因为多线程环境下的因素导致在register方法之前阻塞(一切皆有可能),所以使用wakeup方法做兜底,wakeup像做了一个标记,无论在wakeup前还是后做阻塞了,都可以唤醒一次!

解决方法2:

所以最终解决方案:模仿Netty底层做一个并发队列,把调用register的代码当作一个任务存储到该并发队列中,然后在run方法对应的线程中,再把该队列中存储的register调用代码取出来,这样就保证了register方法和selector.select()在同一个线程中调用。那么你就可以控制先后顺序了。最后再补充一个selector.wakeup()的唤醒作为兜底操作。当selector.select()再次阻塞时,直接唤醒,然后就可以接着在同一线程中执行register方法啦。

具体如下:

ConcurrentLinkedQueue队列可以在两个线程之间进行传递一些代码,功能,知道这些就够了。

测试:

多线程环境下,一切皆有可能,因为你也不知道走到哪一句代码CPU就调度执行其他线程了,所以要考虑更多的情况,然后保证并发安全。

  • 第五版代码(最终代码)
package com.messi.netty_basic_01.Reactor_prac;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class ReactorBossServer2 {

    private static final Logger log = LoggerFactory.getLogger(ReactorBossServer2.class);

    public static void main(String[] args) throws Exception{

        log.debug("boss线程开启 >>>");

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8000));
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //放在外面,做一个共享变量
        Worker2 worker2 = new Worker2("worker-01");

        while (true) {
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);

                    //TODO 调用Worker线程去处理读写事件~~~~
                    log.debug("boss线程即将要调用Worker2类的register方法 >>>");
                    worker2.register(socketChannel);
                    log.debug("boss线程完成调用Worker2类的register方法 >>>");
                }
            }
        }
    }
}
package com.messi.netty_basic_01.Reactor_prac;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;

public class Worker2 implements Runnable{

    private static final Logger log = LoggerFactory.getLogger(Worker2.class);
    private String name;

    private Thread thread ;

    private Selector selector;

    private volatile boolean isCreated;

    public Worker2(String name) {
        this.name = name;
    }

    private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
    public void register(SocketChannel socketChannel) throws Exception{

        if (!isCreated) {
            selector = Selector.open();
            this.thread = new Thread(this,name);
            thread.start();
            isCreated = true;
        }

        log.debug("boss线程将要执行socketChannel#register方法 >>>");

        //使用睡眠的方式模拟出:在register方法执行前执行了繁重的业务代码逻辑
        Thread.sleep(2000);
        log.debug("main线程睡眠 >>>");
//        synchronized (this) {
//            selector.wakeup();
//            socketChannel.register(selector, SelectionKey.OP_READ);
//        }
        queue.add(()->{
            try {
                socketChannel.register(selector,SelectionKey.OP_READ);
            } catch (ClosedChannelException e) {
                throw new RuntimeException(e);
            }
        });
        selector.wakeup();
    }

    @Override
    public void run() {
        while (true) {
            log.debug("开启一个新线程worker-01 >>>");
            try {
                selector.select();

                Runnable poll = queue.poll();
                if (poll != null) {
                    poll.run();
                }

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(30);
                        int read = channel.read(buffer);
                        if (read == -1) {
                            log.debug("客户端断开 >>>");
                            key.cancel();
                        } else {
                            buffer.flip();
                            CharBuffer decode = Charset.forName("UTF-8").decode(buffer);
                            log.info("worker-01线程读取的结果为:{}",decode.toString());
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.messi.netty_basic_01.Reactor_prac;

import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class MyClient2 {

    public static void main(String[] args) throws Exception{
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8000));
        socketChannel.write(Charset.forName("UTF-8").encode("梅西"));
        System.out.println("MyClient2.main");
    }

}

8.2 多线程Reactor模型的实现(一个Boss线程,多个Worker线程)

  • 相比之前的代码,我们要做的就是增加Worker线程的数量 然后随机选举出一个Worker线程进行register

  • 代码
package com.messi.netty_basic_01.reactor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;

public class ReactorBossServer {

    private static final Logger log = LoggerFactory.getLogger(ReactorBossServer.class);


    public static void main(String[] args) throws Exception{
        log.debug("boss线程开启 >>>");
        //建立ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //设置为非阻塞
        ssc.configureBlocking(false);
        //监听端口8000
        ssc.bind(new InetSocketAddress(8000));

        //创建Selector监管者
        Selector selector = Selector.open();
        //把ServerSocketChannel注册到Selector监管者上的keys这一HashSet中,对应注册事件为ACCEPT
        ssc.register(selector, SelectionKey.OP_ACCEPT);

//        Worker worker = new Worker("worker-01");

        //模拟多线程的情况,一个Boss线程,多个Worker线程。在实际开发中,要使用线程池
        Worker[] workers = new Worker[2];
        //模拟出多个Worker线程
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("worker - " + i);
        }

        //原子操作的变量
        AtomicInteger index = new AtomicInteger();


        while (true) {
            //轮询
            selector.select();
            //得到Selector上注册的Channel对应的所有事件 ---》Boss(main)线程对应注册的只有ACCEPT事件,Worker线程处理READ,WRITE事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            //遍历每个事件
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    //拿到的肯定是ACCEPT事件对应的ServerSocketChannel
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    //accept方法接收得到SocketChannel 这个是建立连接后,服务端与客户端之间进行READ或WRITE的通道
                    SocketChannel socketChannel = channel.accept();
                    //同样,设为非阻塞
                    socketChannel.configureBlocking(false);

                    log.debug("boss线程准备调用worker线程的register方法 >>>");

                    //在Boss单线程的情况下,由于是while迭代器逐个遍历,所以这句代码同一时刻只有一个线程访问,不存在并发安全问题
//                    worker.register(socketChannel);

                    //index.getAndIncrement(): 获取到index的值并且+1,线程安全的
                    //index.getAndIncrement() % workers.length 得到的结果为:[0,workers.length-1]
                    //注意哈:一次拿到一个事件去处理,而不是一次拿一堆,由于Boss是单线程, 所以同一时刻register方法只有一个线程去访问,无并发安全问题。
                    workers[index.getAndIncrement() % workers.length].register(socketChannel) ;
                    log.debug("boss线程调用结束worker线程的register方法 >>>");
                }
            }
        }


    }

}
package com.messi.netty_basic_01.reactor;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;

public class Worker implements Runnable{

    private static final Logger log = LoggerFactory.getLogger(Worker.class);
    //worker的名称
    private String name;
    //worker是一个子线程的任务,所以应该有一个线程,其实就是实际干活的
    private Thread thread;

    //每一个worker子线程都会有一个Selector轮询器。一定注意,每一个线程都具有独立的Selector复用器,和主线程不是同一个Selector
    private Selector selector;

    //加上一个volatile来解决多线程环境下isCreated值的可见性问题
    //但是在我们这个程序中,并不存在多线程安全问题,因为Boss线程为单线程,并且迭代器遍历客户端事件时是逐个遍历请求register
    //所以不存在多线程并发安全问题。只不过这里养成良好习惯给标记位加上volatile。为了保证后续多个Boss线程同时访问的环境下的可用性
    private volatile boolean isCreated ; //标记位

    //并发队列,负责传递[代码 或 功能]到另外一个线程
    private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>() ;

    public Worker(String name) {
        this.name = name;
    }

    public void register(SocketChannel socketChannel) throws Exception{
        log.debug("worker#register invoke");
        //使用标记位方式,保证下列初始化只执行一次!
        // 分析一下:为什么只让执行一次?
        // Boss(main)线程调用register方法中的下列这些代码。其实就是Boss线程在与处理完客户端建立连接的事件后的调用
        // 如果每一次处理完一个客户端连接就开启一个新Thread去处理该客户端后续的READ或Write事件,那么是不是倒退了?
        // 所以要控制,每一个Worker对象才会异步开启一个线程去执行处理很多客户端的读写事件,后续压力大了,可以搞一个Worker线程池,多个Worker线程去处理所有客户端的读写事件!
        // 绝对不是一个Worker线程处理一个客户端的读写事件,那样性能耗费太大了,而且没必要!
        // 但是还有一个细节需要思考:
        // 对于同一Worker对象而言,异步线程Thread只创建启动一个。但是对于不同的Worker对象,异步线程Thread会重新创建启动一个
        // 为什么会这样呢?因为isCreated标记位,对于一个新Worker对象,isCreated初始值为false, 那么自然会执行下列这段代码!

        // 下列这个if包围的代码还可以放在哪?
        // 放在Worker的构造方法中,只在对象创建初始化执行一次
        if (!isCreated) {
            //创建线程
            this.thread = new Thread(this,name);
            //先对Selector进行初始化,避免后续NUll Exception
            selector = Selector.open();
            //异步起一个线程
            this.thread.start();
            //标志位设为true 保证初始化代码只执行一次
            isCreated = true;
            System.out.println("我是梅西");
        }
        log.debug("socketChannel#register >>>");

        //使用睡眠模拟繁杂的业务逻辑
        Thread.sleep(2000);
//        synchronized (this) {
//            //其实就是打了一个标记。即调用一次wakeup后,无论此时selector.select()阻塞还是之后才阻塞,都会减少阻塞一次
//            selector.wakeup();
//            socketChannel.register(selector, SelectionKey.OP_READ);
//        }

        //把SocketChannel注册到Selector并且设置READ事件的代码放到并发队列中 传递给后续新开启的线程
        queue.add(()->{
            try {
                socketChannel.register(selector, SelectionKey.OP_READ);
            } catch (ClosedChannelException e) {
                e.printStackTrace();
            }
        });
        //兜底操作:唤醒一次Selector。无论Selector在wakeup方法执行前阻塞,还是在wakeup执行后阻塞,wakeup都可以唤醒一次Selector阻塞,像是一个标记计数
        selector.wakeup();
        System.out.println("Worker.register");
    }

    @Override
    public void run() {
        while (true) {
            log.debug("worker线程的run方法开始执行 >>>");
            try {
                //应用层等待。内核层轮询监听事件的发生
                selector.select();

                //从并发队列中获取任务
                Runnable poll = queue.poll();
                //任务不为空,则执行该任务
                if (poll != null) {
                    poll.run();
                }
                //一旦Selector监听到keys这一HashSet中注册的Channel对应事件的发生,就会把这些触发的事件对应的Channel的引用复制copy到SelectionKeys这一HashSet中
                //这一copy迁移Channel引用的工作是selector.select()做的
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                //遍历Selector监听到的事件集合迭代器
                while (iterator.hasNext()) {
                    //一个一个的来
                    SelectionKey key = iterator.next();
                    //使用完后就删除掉 避免空指针异常
                    iterator.remove();
                    //Worker线程监听Read事件
                    if (key.isReadable()) {
                        //获取到SocketChannel 该SocketChannel为客户端与服务端之间进行读写操作的管道
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        //创建Buffer缓冲区 默认为写模式
                        ByteBuffer buffer = ByteBuffer.allocate(30);
                        //把SocketChannel中的数据读取到buffer缓冲区
                        socketChannel.read(buffer);
                        //切换到读模式
                        buffer.flip();
                        //解码
                        CharBuffer result = Charset.forName("UTF-8").decode(buffer);
                        //输出读取到的结果数据
                        log.info("worker线程读取的结果result为 : {}",result);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.messi.netty_basic_01.reactor;

import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class MyClient {

    public static void main(String[] args) throws Exception{
        //客户端创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //客户端与服务端之间通过SocketChannel进行连接
        socketChannel.connect(new InetSocketAddress(8000));
        //客户端写数据给服务端
        socketChannel.write(Charset.forName("UTF-8").encode("leomessi")) ;
        //.....
        System.out.println("MyClient.main");
    }

}
  • 测试

debug的方式进行开启多个客户端与服务端建立通信

Server端:每次都会选一个Worker线程执行register方法

  • 讨论交流

worker线程搞多少个比较好 ? ? ?

CPU核数减1。现代计算机一般都是单CPU多个核,也就是俗称的多核CPU。假设目前为八核,那么开启8-1=7个worker线程,为什么开7个worker线程,一个线程给boss线程,一共8个线程,更好的利用这八个核,实现真正意义上的并行。截止到目前有好几个问题:(1)为什么worker线程数要多于boss线程?(2)为什么正好开启核数个线程效率最佳?(即核数=boss线程数+worker线程数)(3)能不能开启线程个数超过最大核数,最大开启线程数为多少?

依次回答:(1)对于一个客户端而言,连接只进行一次,连接之后的读写可能回触发多次!而我们又知道,boss线程是处理连接的,worker线程处理连接后的读写事件的,答案显而易见了。(2)为了实现真正意义上的并行,因为当线程数超过CPU核数时,CPU需要不断的频繁调度切换。并且线程创建是占用内存的,线程越多,内存消耗越大。(3)当然可以,最大开启线程数取决于具体硬件配置,你要看硬件的并发能力,你要CPU切换能力强,内存大,线程开多点其实也没毛病的。

9.零拷贝

一定要看这篇文章:

一文彻底弄懂零拷贝原理 - 掘金

  • 基本知识

无论是磁盘IO还是网络IO,实际上最终都需要操作具体的硬件,磁盘IO--->磁盘文件,网络IO--->网络网卡

但是呢,我们编写的JVM应用程序是无法直接操控硬件的,中间需要通过操作系统做中间者,具体原因这里简单说一下,因为对于磁盘,网络资源,我们不能让用户程序直接操控,一方面是硬件版本问题,一方面是安全问题(怎么可能让用户直接操控内存呢,这多不安全呀!)

具体的调用过程为:JVM用户程序--->操作系统--->硬件驱动程序--->具体的硬件

总结一下:

1.JVM中的java应用程序不能直接操作硬件,需要操作系统的介入

2.内存分为两种,也可以理解成内存空间分为两部分:

一部分为用户地址空间,这部分的内存空间通常给应用程序(如:JVM进行使用),应用程序通常是进程级别(我们常说的线程,线程实际上是共享进程的内存资源的一种抽象细化,线程是利用进程空间的)。应用程序其实就是我们平常编写的各种代码动态运行起来的一种概念统称。

另外一部分为内核地址空间,这部分的内存空间通常给操作系统,操作系统也是占用内存的一种特殊的程序,操作系统通常会提供一系列公共的功能API。啥是公共的功能API?比如:应用程序需要申请内存空间,应用程序就需要先调用操作系统提供的公共功能API。

3.为什么socket网络连接,io流资源,线程资源都是珍贵的资源,使用完毕后要及时关闭?

对于这些资源,是对安全性要求极高的资源,你仅仅通过用户程序是无法完成这些资源的操控的,必须陷入操作系统内核让OS去真正调用申请。所以这些资源是珍贵的, 是位于OS内存的公共资源。

并且申请的这些公共资源都是占用着操作系统所对应的内核地址空间内存,我们应用程序是陷入内核使用这部分内核内存空间的。

假如说当前用户程序使用完毕后不断开对这部分操作系统内存的引用(即不断开socket网络连接,IO流),会影响这台计算机其他应用程序对操作系统这部分公共资源内存的调用和使用的,因为不可能一台计算机只有一个进程应用程序!甚至导致其他应用程序无法申请到这些珍贵的资源。所以使用完后,应用程序要及时断开对珍贵资源的连接。

并且要减少应用程序与这部分资源的交互,因为需要陷入内核,切换的时候性能耗费一定是很大的

  • 传统方式进行拷贝,不进行零拷贝优化:

下面分析涉及到的代码程序

总结:

参看文章:一文彻底弄懂零拷贝原理 - 掘金

分析:

注释:创建直接内存的API就是:ByteBuffer buffer = ByteBuffer.allocateDirect(20);

  • 零拷贝优化1:mmap+write

涉及到的代码:

通过NIO的API创建直接内存空间,所谓直接内存就是应用程序不再开辟用户地址内存空间(堆内存)了,而是直接使用操作系统的内存。用户程序可以编写代码直接操控这一块直接内存,可以读写,清除等操作。

这也叫做内存映射,好像是把操作系统的内存映射到用户地址内存空间上,减少了数据的拷贝。

但应用程序一味的使用直接内存也存在很大弊端,因为OS不具备JVM的GC机制,所以如果应用程序不及时释放操作系统的直接内存,那么可能会存在很大隐患。

eg:调用NIO-API:ByteBuffer.allocateDirect(10),这句代码创建的就是10字节大小的操作系统内存:高速页缓存这种。然后应用程序获取到这块直接内存后,在应用程序中就可以使用java代码直接操作这块直接内存了。

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

补充:

但是注意,内存映射/开辟OS直接内存是针对于文件的读写操作。对于网络相关的数据读写操作我们不可以使用直接内存映射,为什么?因为你无法把这一台JVM的相应内容数据给另外一台JVM。

  • 零拷贝优化2:linux内核2.1版本的零拷贝 - sendfile方式

涉及到的代码:

所有的零拷贝都是文件相关的,socket不涉及零拷贝。零拷贝是指不进行JVM虚拟机层面的拷贝,只进行系统级别的拷贝.

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 零拷贝优化3:linux内核2.4版本的零拷贝- 带有 scatter/gather 的 sendfile方式

对Linux2.4内核进行sendFile方法的优化的一些理解:这次优化直接让保存到高速页缓存的文件读取数据直接发送到网卡对应的驱动程序,再给到网卡。而无需拷贝到socket缓存。

为什么可以这样?因为无论是文件对应的高速页缓存还是网卡对应的socket缓存,其实内部结构都是一样的,只不过可能标记位不同,既然差不多一样,把页缓存的内存地址,偏移量记录传输到socket缓存,然后直接把高速页缓存的数据给到网卡驱动程序,然后再给网卡,这有问题吗?

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 零拷贝优化4 - splice 方式

splice 调用和sendfile 非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 总结

无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。

sendfile局限于文件与socket进行数据传输。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。

下图展示了各种零拷贝技术的对比图:

  • 补充

我们java平常做的优化都是在JVM层面进行优化的。对于OS操作系统层面的优化,需要OS开发维护者去优化的,比如说linux2.4内核完成了sendFile方法的重构,epoll模型的完善。对于我们java程序来说,只要你部署在linux操作系统上,你就可以享用OS优化带来的好处,因为你java程序对于一些系统级别的操作,如文件读取,其实是调用OS的sendFile,那么OS对sendFile做了优化,是不是等价于你java程序性能也进一步优化了?对吧,答案显而易见。

这就是为什么后面我们的服务一般都部署在linux操作系统服务器上,它在IO处理方面是要比市面上其他操作系统服务器要强的多的,但是异步处理层面上要比win操作系统差一些的。。。所以现在都研究linux内核

10.硬件驱动程序与软件驱动程序(数据库驱动程序)的关系

11.linux内核与centos,redhat等这些产品有啥区别和关系?

linux内核是开源的,是linux最核心的代码。后续centos,redhat这些厂商基于linux内核进行开发,进行套壳,增加了稳定性,可靠性,或一些美丽的图形化界面。但是这些厂商也会把产品分为很多发行版,比如说这一个发行版做的不好或差一些,那么就开源不收费。如果这一个发行版做的好,那么就收费闭源。。。

这就好比:安卓操作系统与小米的MIUI,华为的一些发行版产品等,一样的关系。

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

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

相关文章

GEE数据——美国农业部LANDFIRE (LF)数据集2.3.0版本

地面火灾数据集 LANDFIRE (LF)&#xff0c;即 "地貌火灾和资源管理规划工具"&#xff0c;是美国农业部森林服务局、美国内政部地质调查局和大自然保护协会的野地火灾管理项目之间的共享项目。前言 – 人工智能教程 LANDFIRE (LF) 图层是利用基于大量实地参考数据、…

【JS逆向九】逆向某混淆网站源码,模拟 加密,解密,密钥生成

逆向日期&#xff1a;2024.02.09 使用工具&#xff1a;Node.js 是否有混淆&#xff1a;源代码混淆 加密方法&#xff1a;AES标准库 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 可使用AES进行解密处理&#xff08;直接解密即可&#xff0…

【iOS分类、关联对象】如何使用关联对象给分类实现一个weak的属性

如何使用关联对象给分类实现一个weak的属性 通过关联对象objc_setAssociatedObject中的策略policy可知&#xff0c;并不支持使用weak修饰对象属性&#xff1a; typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {OBJC_ASSOCIATION_ASSIGN 0, //assignOBJC_ASSOCIATION…

物品冷启动01_优化目标评价(包括基尼系数)

文章目录 物品冷启动冷启动的类型“新”按常规推送链路的角度按产品生态角度 物品冷启动的目标和评价指标作者侧用户侧 冷启动的衡量 物品冷启动 冷启动的类型 冷启动的内容种类包括很多方面&#xff0c;本文只介绍UGC的冷启动。 所谓UGC&#xff0c;就是User Generate Conte…

哈工大团队顶刊发布!由单偏心电机驱动的爬行机器人实现多方向运动传递

单电机也能驱动平面内前进和转弯运动&#xff1f;没错&#xff0c;图中的机器人名叫GASR&#xff0c;仅由四个零件组成&#xff0c;分别是偏心电机、电池、电路板、聚酰亚胺薄片&#xff0c;它可以灵活自如地实现前进、转弯等移动。其中的核心驱动器——纽扣式偏心转子电机产自…

【机器学习笔记】回归算法

回归算法 文章目录 回归算法1 线性回归2 损失函数3 多元线性回归4 线性回归的相关系数 1 线性回归 回归分析(Regression) 回归分析是描述变量间关系的一种统计分析方法 例&#xff1a;在线教育场景 因变量 Y&#xff1a;在线学习课程满意度 自变量 X&#xff1a;平台交互性、教…

【c++】c++入门(上)

0.前言 由于c完全是由c语言演变而来&#xff0c;所以c是完全兼容c语言的&#xff0c;c语言中的语法都可在c中使用&#xff0c;但正因为c语言有很多语法的不足&#xff0c;我们的祖师爷&#xff0c;也就是c之父增加了一些可以补足c语言的不足之处&#xff0c;c在c语言的基础上增…

树莓派的pip安装时候添加清华源

每次都要去找镜像网址&#xff0c;太麻烦了&#xff0c;通过改配置可以一次性解决。 首先创建一个.pip 目录 mkdir ~/.pip意味着在当前目录下创建.pip文件&#xff0c;不过这个是隐藏文件&#xff0c;一般情况下是关闭隐藏文件的可视的&#xff0c;于是我绕了点弯弯。 编辑…

“OLED屏幕,色彩绚丽,画面清晰,让每一帧都生动无比。“#IIC协议【下】

"OLED屏幕&#xff0c;色彩绚丽&#xff0c;画面清晰&#xff0c;让每一帧都生动无比。"#IIC协议【下】 前言预备知识1. OLED显示一个点代码实现1.1 OLED显示一个点代码实现核心思路1.2和LCD1602一样需要初始化&#xff0c;看手册&#xff0c;写初识化函数1.3选择Pag…

Redis -- 渐进式遍历

家&#xff0c;是心的方向。不论走多远&#xff0c;总有一盏灯为你留着。桌上的碗筷多了几双&#xff0c;笑声也多了几分温暖。家人团聚&#xff0c;是最美的风景线。时间&#xff1a;2024年 2月 8日 12:51:20 目录 前言 语法 示例 前言 试想一个场景,那就是在key非常多的…

python 自我检测题--part 1

1. Which way among them is used to create an event loop ? Window.mainloop() 2. Suppose we have a set a {10,9,8,7}, and we execute a.remove(14) what will happen ? Key error is raised. The remove() method removes the specified element from the set. Th…

攻防世界——re2-cpp-is-awesome

64位 我先用虚拟机跑了一下这个程序&#xff0c;结果输出一串字符串flag ——没用 IDA打开后 F5也没有什么可看的 那我们就F12查看字符串找可疑信息 这里一下就看见了 __int64 __fastcall main(int a1, char **a2, char **a3) {char *v3; // rbx__int64 v4; // rax__int64 v…

DS:顺序栈的实现

创作不易&#xff0c;友友们给个三连吧&#xff01;&#xff01; 一、栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先…

test222

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 磁盘满的本质分析 专栏&#xff1a;《Linux从小白到大神》 | 系统学习Linux开发、VIM/GCC/GDB/Make工具…

【SpringBoot】Redis集中管理Session和自定义用户参数解决登录状态及校验问题

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java对AI的调用开发》 《RabbitMQ》《Spring》《SpringMVC》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 前言一、分布…

【C语言】一道相当有难度的指针某大厂笔试真题(超详解)

这是比较复杂的题目&#xff0c;但是如果我们能够理解清楚各个指针代表的含义&#xff0c;画出各级指针的关系图&#xff0c;这道题就迎刃而解了。 学会这道笔试题&#xff0c;相信你对指针的理解&#xff0c;对数组&#xff0c;字符串的理解都会上一个档次。 字符串存储使用的…

Linux之umask的使用

一、umask的作用 umask值用于设置用户在创建新文件和目录时的默认权限。umask值一共有4组数字&#xff0c;其中第1组数字用于定义特殊权限&#xff0c;一般不关心&#xff0c;日常工作中大家用的更多的是后面三组数字。以下图为例&#xff0c;输入“umask”命令之后&#xff0c…

《动手学深度学习(PyTorch版)》笔记7.7

注&#xff1a;书中对代码的讲解并不详细&#xff0c;本文对很多细节做了详细注释。另外&#xff0c;书上的源代码是在Jupyter Notebook上运行的&#xff0c;较为分散&#xff0c;本文将代码集中起来&#xff0c;并加以完善&#xff0c;全部用vscode在python 3.9.18下测试通过&…

DolphinScheduler-3.2.0 集群搭建

本篇文章主要记录DolphinScheduler-3.2.0 集群部署流程。 注&#xff1a;参考文档&#xff1a; DolphinScheduler-3.2.0生产集群高可用搭建_dophinscheduler3.2.0 使用说明-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞25次&#xff0c;收藏23次。DolphinScheduler-3.2.0生产…

MySQL篇----第十八篇

系列文章目录 文章目录 系列文章目录前言一、SQL 语言包括哪几部分?每部分都有哪些操作关键二、完整性约束包括哪些?三、什么是锁?四、什么叫视图?游标是什么?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,…
最新文章