>  기사  >  웹 프론트엔드  >  Nodejs의 버퍼 캐시 영역에 대한 심층적인 이해

Nodejs의 버퍼 캐시 영역에 대한 심층적인 이해

青灯夜游
青灯夜游앞으로
2021-07-21 10:40:132313검색

이 기사에서는 NodeJs의 버퍼 캐시 영역을 이해하고 Node.js의 버퍼 구현을 소개합니다.

Nodejs의 버퍼 캐시 영역에 대한 심층적인 이해

관련 지식 포인트

ArrayBuffer

먼저 JavaScript에서 ArrayBuffer의 인터페이스와 배경에 대해 이야기해 보겠습니다. 다음 내용은 ECMAScript 6 Getting Started with ArrayBuffer 에서 가져온 것입니다. .

ArrayBuffer 객체, TypedArray 뷰 및 DataView 뷰는 JavaScript가 바이너리 데이터를 조작하기 위한 인터페이스입니다. 이러한 객체는 오랫동안 별도의 사양(2011년 2월 출시)으로 존재해 왔으며 ES6에서는 이를 ECMAScript 사양에 통합하고 새로운 메서드를 추가했습니다. 이들은 모두 배열 구문을 사용하여 이진 데이터를 처리하므로 집합적으로 이진 배열이라고 합니다.

이 인터페이스의 원래 디자인 목적은 WebGL 프로젝트와 관련이 있습니다. 소위 WebGL은 브라우저와 그래픽 카드 사이의 통신 인터페이스를 의미합니다. JavaScript와 그래픽 카드 간의 대규모 실시간 데이터 교환을 충족하려면 둘 사이의 데이터 통신이 바이너리가 아닌 바이너리여야 합니다. 전통적인 텍스트 형식. 32비트 정수가 텍스트 형식으로 전달되면 양쪽 끝의 JavaScript 스크립트와 그래픽 카드가 형식 변환을 수행해야 하므로 시간이 많이 걸립니다. 이때 C언어처럼 바이트를 직접 연산하여 4바이트 32비트 정수를 바이너리 형태로 그대로 그래픽 카드에 보낼 수 있는 메커니즘이 있다면 스크립트의 성능은 크게 향상될 것이다.

이진 배열은 이러한 맥락에서 탄생했습니다. 이는 C 언어의 배열과 매우 유사하여 개발자가 배열 첨자 형태로 메모리를 직접 조작할 수 있게 하며, 이는 JavaScript의 바이너리 데이터 처리 능력을 크게 향상시켜 개발자가 C 언어의 기본 인터페이스와 바이너리 통신을 수행할 수 있게 해줍니다. JavaScript를 통한 운영 체제.

이 글을 읽고 나면 ArrayBuffer 시리즈 인터페이스를 통해 JavaScript가 바이너리 데이터를 처리할 수 있다는 것을 알 수 있습니다. 사용법은 주로 다음 단계로 나뉩니다.

  • ArrayBuffer 생성자를 통해 길이 10의 메모리 영역을 만듭니다

  • ArrayBuffer를 가리키도록 Uint8Array 생성자를 통해 매개변수 전달

  • 배열을 작동하는 것처럼 첫 번째 바이트 123에 데이터 쓰기

const buf1 = new ArrayBuffer(10);
const x1 = new Uint8Array(buf1);
x1[0]  = 123;

Buffer

ArrayBuffer는 Node.js에서도 사용할 수 있습니다. 처리할 관련 인터페이스 ArrayBufferBuffer 문서를 주의 깊게 읽은 후 Buffer를 추가로 캡슐화하면 더 쉽게 시작하고 더 나은 성능을 얻을 수 있다는 것을 알 수 있습니다. 그런 다음 Buffer

의 사용 예를 살펴보겠습니다.
  • alloc 메소드를 통해 길이 10의 메모리 영역을 생성합니다

  • writeUInt8을 통해 첫 번째 바이트에 데이터 123을 씁니다

  • readUint8

const buf1 = Buffer.alloc(10);
buf1.writeUInt8(123, 0)
buf1.readUint8(0)

Buffer를 통해 데이터의 첫 번째 바이트를 읽습니다.

정적 메서드 alloc을 통해 Buffer 인스턴스 생성

팁: Buffer 생성자를 통해 직접 인스턴스를 생성하는 방법은 보안 문제로 인해 중단되었습니다.

Buffer.alloc = function alloc(size, fill, encoding) {
  assertSize(size);
  if (fill !== undefined && fill !== 0 && size > 0) {
    const buf = createUnsafeBuffer(size);
    return _fill(buf, fill, 0, buf.length, encoding);
  }
  return new FastBuffer(size);
};

class FastBuffer extends Uint8Array {
  constructor(bufferOrLength, byteOffset, length) {
    super(bufferOrLength, byteOffset, length);
  }
}

Buffer가 실제로는 Uint8Array라는 것을 확인했습니다. JavaScript에서도 사용된다는 점을 추가하세요. ArrayBuffer 객체를 사용하지 않고 Uint8Array를 사용하여 직접 메모리를 조작할 수 있습니다. 예를 들어 다음 예제에서는 Uint8Array 생성자를 통해 길이가 10인 메모리 영역을 생성합니다.

    const x1 = new Uint8Array(10);
    x1[0] = 123
  • 그래서 Node.js의 Buffer는 Uint8Array 클래스만 사용합니다. 다음의 모든 뷰 유형의 동작을 시뮬레이션하는 방법과 Buffer에 적용된 다른 확장은 무엇입니까?

    • Int8Array:8 位有符号整数,长度 1 个字节。
    • Uint8Array:8 位无符号整数,长度 1 个字节。
    • Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不同。
    • Int16Array:16 位有符号整数,长度 2 个字节。
    • Uint16Array:16 位无符号整数,长度 2 个字节。
    • Int32Array:32 位有符号整数,长度 4 个字节。
    • Uint32Array:32 位无符号整数,长度 4 个字节。
    • Float32Array:32 位浮点数,长度 4 个字节。
    • Float64Array:64 位浮点数,长度 8 个字节。

    allocUnsafe, allocUnsafeSlow

    提供了 alloc, allocUnsafe, allocUnsafeSlow 3个方法去创建一个 Buffer 实例, 上面讲了 alloc 方法没有什么特别, 下面讲一下另外两种方法

    allocUnsafe

    与 alloc 不同的是, allocUnsafe 并没有直接返回 FastBuffer, 而是始终从 allocPool 中类似 slice 出来的内存区。

    Buffer.allocUnsafe = function allocUnsafe(size) {
      assertSize(size);
      return allocate(size);
    };
    
    function allocate(size) {
      if (size <= 0) {
        return new FastBuffer();
      }
      if (size < (Buffer.poolSize >>> 1)) {
        if (size > (poolSize - poolOffset))
          createPool();
        const b = new FastBuffer(allocPool, poolOffset, size);
        poolOffset += size;
        alignPool();
        return b;
      }
      return createUnsafeBuffer(size);
    }

    这块内容其实我也是很早之前在读朴灵大佬的深入浅出 Node.js 就有所映像, 为什么这样做了, 原因主要如下

    为了高效地使用申请来的内存,Node采用了slab分配机制。slab是一种动态内存管理机制,最早

    诞生于SunOS操作系统(Solaris)中,目前在一些*nix操作系统中有广泛的应用,如FreeBSD和Linux。 简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态。

    • full:完全分配状态。
    • partial:部分分配状态。
    • empty:没有被分配状态。

    当我们需要一个Buffer对象,可以通过以下方式分配指定大小的Buffer对象:

    new Buffer(size); Node以8 KB为界限来区分Buffer是大对象还是小对象: Buffer.poolSize = 8 * 1024; 这个8 KB的值也就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配。

    allocUnsafeSlow

    比起 allocUnsafe 从预先申请好的 allocPool 内存中切割出来的内存区, allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域。从命名可知直接使用 Uint8Array 等都是 Slow 缓慢的。

    Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
      assertSize(size);
      return createUnsafeBuffer(size);
    };

    createUnsafeBuffer

    这个 Unsafe 不安全又是怎么回事了, 其实我们发现直接通过 Uint8Array 申请的内存都是填充了 0 数据的认为都是安全的, 那么 Node.js 又做了什么操作使其没有被填充数据了 ?

    let zeroFill = getZeroFillToggle();
    function createUnsafeBuffer(size) {
      zeroFill[0] = 0;
      try {
        return new FastBuffer(size);
      } finally {
        zeroFill[0] = 1;
      }
    }

    那么我们只能去探究一下 zeroFill 在创建前后, 类似开关的操作的是如何实现这个功能

    getZeroFillToggle

    zeroFill 的值来自于 getZeroFillToggle 方法返回, 其实现在 src/node_buffer.cc 文件中, 整个看下来也是比较费脑。

    简要的分析一下 zeroFill 的设置主要是修改了 zero_fill_field 这个变量的值, zero_fill_field 值主要使用在 Allocate 分配器函数中。

    void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
      Environment* env = Environment::GetCurrent(args);
      NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
      Local<ArrayBuffer> ab;
      // It can be a nullptr when running inside an isolate where we
      // do not own the ArrayBuffer allocator.
      if (allocator == nullptr) {
        // Create a dummy Uint32Array - the JS land can only toggle the C++ land
        // setting when the allocator uses our toggle. With this the toggle in JS
        // land results in no-ops.
        ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
      } else {
        uint32_t* zero_fill_field = allocator->zero_fill_field();
        std::unique_ptr<BackingStore> backing =
            ArrayBuffer::NewBackingStore(zero_fill_field,
                                         sizeof(*zero_fill_field),
                                         [](void*, size_t, void*) {},
                                         nullptr);
        ab = ArrayBuffer::New(env->isolate(), std::move(backing));
      }
    
      ab->SetPrivate(
          env->context(),
          env->untransferable_object_private_symbol(),
          True(env->isolate())).Check();
    
      args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
    }

    Allocate

    内存分配器的实现

    从代码实现可以看到如果 zero_fill_field 值为

    • 真值的话会调用 UncheckedCalloc 去分配内存
    • 假值则调用 UncheckedMalloc 分配内存
    void* NodeArrayBufferAllocator::Allocate(size_t size) {
      void* ret;
      if (zero_fill_field_ || per_process::cli_options->zero_fill_all_buffers)
        ret = UncheckedCalloc(size);
      else
        ret = UncheckedMalloc(size);
      if (LIKELY(ret != nullptr))
        total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
      return ret;
    }

    UncheckedCalloc UncheckedMalloc

    接着 Allocate 函数的内容

    • zero_fill_field 为真值的话会调用 UncheckedCalloc, 最后通过 calloc 去分配内存
    • zero_fill_field 为假值则调用 UncheckedMalloc, 最后通过 realloc 去分配内存

    关于 calloc 与 realloc 函数

    • calloc: calloc 函数得到的内存空间是经过初始化的,其内容全为0
    • realloc: realloc 函数得到的内存空间是没有经过初始化的

    至此读到这里, 我们知道了 createUnsafeBuffer 创建未被初始化内存的完整实现, 在需要创建时设置 zero_fill_field 为 0 即假值即可, 同步创建成功再把 zero_fill_field 设置为 1 即真值就好了。

    inline T* UncheckedCalloc(size_t n) {
      if (n == 0) n = 1;
      MultiplyWithOverflowCheck(sizeof(T), n);
      return static_cast<T*>(calloc(n, sizeof(T)));
    }
    
    template <typename T>
    inline T* UncheckedMalloc(size_t n) {
      if (n == 0) n = 1;
      return UncheckedRealloc<T>(nullptr, n);
    }
    
    template <typename T>
    T* UncheckedRealloc(T* pointer, size_t n) {
      size_t full_size = MultiplyWithOverflowCheck(sizeof(T), n);
    
      if (full_size == 0) {
        free(pointer);
        return nullptr;
      }
    
      void* allocated = realloc(pointer, full_size);
    
      if (UNLIKELY(allocated == nullptr)) {
        // Tell V8 that memory is low and retry.
        LowMemoryNotification();
        allocated = realloc(pointer, full_size);
      }
    
      return static_cast<T*>(allocated);
    }

    其他实现

    通过 Uint8Array 如何写入读取 Int8Array 数据? 如通过 writeInt8 写入一个有符号的 -123 数据。

    const buf1 = Buffer.alloc(10);
    buf1.writeInt8(-123, 0)

    writeInt8, readInt8

    • 对写入的数值范围为 -128 到 127 进行了验证

    • 直接进行赋值操作

    其实作为 Uint8Array 对应的 C 语言类型为 unsigned char, 可写入的范围为 0 到 255, 当写入一个有符号的值时如 -123, 其最高位符号位为 1, 其二进制的原码为 11111011, 最终存储在计算机中所有的数值都是用补码。所以其最终存储的补码为 10000101, 10 进制表示为 133。

    • 此时如果通过 readUInt8 去读取数据的话就会发现返回值为 133

    • 如果通过 readInt8 去读取的话, 套用代码的实现 133 | (133 & 2 ** 7) * 0x1fffffe === -123 即满足要求

    function writeInt8(value, offset = 0) {
      return writeU_Int8(this, value, offset, -0x80, 0x7f);
    }
    
    function writeU_Int8(buf, value, offset, min, max) {
      value = +value;
      // `checkInt()` can not be used here because it checks two entries.
      validateNumber(offset, &#39;offset&#39;);
      if (value > max || value < min) {
        throw new ERR_OUT_OF_RANGE(&#39;value&#39;, `>= ${min} and <= ${max}`, value);
      }
      if (buf[offset] === undefined)
        boundsError(offset, buf.length - 1);
    
      buf[offset] = value;
      return offset + 1;
    }
    
    function readInt8(offset = 0) {
      validateNumber(offset, &#39;offset&#39;);
      const val = this[offset];
      if (val === undefined)
        boundsError(offset, this.length - 1);
    
      return val | (val & 2 ** 7) * 0x1fffffe;
    }

    计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。

    通过 Uint8Array 如何写入读取 Uint16Array  数据?

    writeUInt16, readUInt16

    从下面的代码也是逐渐的看清了 Uint8Array 的实现, 如果写入 16 位的数组, 即会占用两个字节长度的 Uint8Array, 每个字节存储 8 位即可。

    function writeU_Int16BE(buf, value, offset, min, max) {
      value = +value;
      checkInt(value, min, max, buf, offset, 1);
    
      buf[offset++] = (value >>> 8);
      buf[offset++] = value;
      return offset;
    }
    
    function readUInt16BE(offset = 0) {
      validateNumber(offset, &#39;offset&#39;);
      const first = this[offset];
      const last = this[offset + 1];
      if (first === undefined || last === undefined)
        boundsError(offset, this.length - 2);
    
      return first * 2 ** 8 + last;
    }

    BE 指的是大端字节序, LE 指的是小端字节序, 使用何种方式都是可以的。小端字节序写用小端字节序读, 端字节序写就用大端字节序读, 读写规则不一致则会造成乱码, 更多可见 理解字节序

    • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
    • 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

    writeFloatForwards, readFloatForwards

    对于 float32Array 的实现, 相当于直接使用了 float32Array

    • 写入一个数值时直接赋值给 float32Array 第一位, 然后从 float32Array.buffe 中取出写入的 4 个字节内容
    • 读取时给 float32Array.buffe 4个字节逐个赋值, 然后直接返回 float32Array 第一位即可
    const float32Array = new Float32Array(1);
    const uInt8Float32Array = new Uint8Array(float32Array.buffer);
    
    function writeFloatForwards(val, offset = 0) {
      val = +val;
      checkBounds(this, offset, 3);
    
      float32Array[0] = val;
      this[offset++] = uInt8Float32Array[0];
      this[offset++] = uInt8Float32Array[1];
      this[offset++] = uInt8Float32Array[2];
      this[offset++] = uInt8Float32Array[3];
      return offset;
    }
    
    function readFloatForwards(offset = 0) {
      validateNumber(offset, &#39;offset&#39;);
      const first = this[offset];
      const last = this[offset + 3];
      if (first === undefined || last === undefined)
        boundsError(offset, this.length - 4);
    
      uInt8Float32Array[0] = first;
      uInt8Float32Array[1] = this[++offset];
      uInt8Float32Array[2] = this[++offset];
      uInt8Float32Array[3] = last;
      return float32Array[0];
    }

    小结

    本文主要讲了 Node.js 中 Buffer 的实现, 相比直接使用 Uint8Array 等在性能安全以及使用上方便层度上做了一些改造, 有兴趣的同学可以扩展阅读 gRPC 中的 Protocol Buffers 的实现, 其遵循的是 Varints 编码 与 Zigzag 编码实现。

    更多编程相关知识,请访问:编程视频!!

위 내용은 Nodejs의 버퍼 캐시 영역에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제