Python語言與C語言數據交互的場景還是比較常見的,例如在使用python優秀的數據處理可視化等優勢的同時,對於某些優秀的開源C/C++的軟件庫的調用就需要用到ctypes庫函數對動態庫進行API的靈活調用了,再例如在某些場景下,C語言的數據需要可視化,而C語言的可視化接口的支持是很薄弱的,這裏可以採用Python強大的可視化效果來驗證數據的正確性(也可以採用MATLAB完成可視化)。 再比如在某些Demo測試場景下,使用Python便捷部署的神經網絡框架在使用獲取內存圖像數據時也可能使用到Python和C之間數據交換的需求。
一、python與C交互的重要庫 ctypes
ctypes庫作爲python與C之間的交互的重要庫,其定義了各類數據類型與C語言中的數據類型進行對應,其中包括了char,int,POINTER等等,具體可以參看數據手冊。ctypes還能夠通過CDLL接口應用C語言的動態庫.so,在調用接口過程中,應該要嚴格配置Python端調用動態庫函數接口的參數類型(不能有任何偏差),應該掌握如何定義數組緩衝區並操作地址變量,同時能夠使用接口獲取變量地址,從而傳給C-API使用,基礎如下所示:
1. 數組類型定義及使用
測試代碼:
1 # 特殊ctypes類型數組類型定義 2 ArrayTestType = ctypes.c_uint8 * 10 # 重定義數據類型 3 ArrayTest = ArrayTestType(11) # 定義數據並初始化數據賦值爲11 4 ArrayTest[5] = 20 # 對數據進行賦值處理 5 print('Line', sys._getframe().f_lineno, ':', ArrayTest, type(ArrayTest), ArrayTestType, type(ArrayTestType)) 6 print('Line', sys._getframe().f_lineno, ':', ArrayTest[0], ArrayTest[5])
運行結果:
Line 20 : <__main__.c_ubyte_Array_10 object at 0x7fd1318e8ea0> <class '__main__.c_ubyte_Array_10'> <class '__main__.c_ubyte_Array_10'> <class '_ctypes.PyCArrayType'> Line 21 : 11 20
這裏可以看到 ArrayTestType 的類型其實爲 PyCArrayType PythonC數組類型,因此 ArrayTestType 可以定義一個 unsigned char Array[10] 的數組。
注:數組類型的縮略定義方法(相當於上述步驟的兩步)
# 定義一個大小爲10指針實例作爲緩存, 等效爲 Step1:DataType = c_uint8 * LENGTH --> Step2:DataPoint = DataType() DataPoint = (c_uint8 * LENGTH)()
2. 動態庫的加載及接口調用
測試代碼:
1 # 從C語言或者C++的動態庫當中加載C函數以調用 2 p = os.getcwd() + '/libfunc.so' # 獲取當前的動態庫的絕對路徑位置 3 f = cdll.LoadLibrary(p) # 使用LoadLibrary接口加載C語言動態庫 4 5 # Python Class類 到 C struct結構體 數據類型的傳輸轉換 6 Sfunction = f.py_struct_address # 從當前庫當中取得clib中的函數py_struct_address,並重命名爲Sfunction 7 Sfunction.argtypes = [POINTER(POINT), POINTER(ctypes.c_char)] # 設置當前函數的輸入參數
這裏使用了 os模塊獲取了動態庫的絕對路徑,並調用cdll.LoadLibrary(libpath),這裏使用 argtypes 定義了動態庫函數的輸入參數類型,分別爲結構體指針、char *指針。
3. 定義字符串類型(字符串string)
1 p = create_string_buffer(b"Hello World", 15) # create a 10 byte buffer 2 print('Line', sys._getframe().f_lineno, ':', p,sizeof(p), repr(p.raw))
創建一個string類型的緩衝空間,並返回一個字符串指針指向這串字符串,create_string_buffer 參數分別爲字符串、buffer總長度,這個長度不能小於前一個參數字符串的長度大小,否則會報錯,p.raw取字符串的內容。
4. Numpy數組的地址獲取及應用
測試代碼:
1 # Numpy 數據類型等相互轉換測試(將內存數據轉換值Python當中) 2 ImgW = 1669 # 圖像寬度 3 ImgH = 21 # 圖像高度 4 ImgC = 3 # 圖像通道數 5 ImgL = ImgW*ImgH*ImgC # 圖像總長度 6 7 ImgArray = np.zeros((ImgW,ImgH,ImgC), dtype=np.ubyte) # 申請圖像總空間爲多維 zeros 矩陣 8 print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))) # 將numpy數組轉換爲地址表示方式 data_as 並打印數據類型 9 ImgArray_addr = ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) # 獲取的指針放置在ImgArray_addr變量當中 10 print('Line', sys._getframe().f_lineno, ':', ImgArray_addr[0],ImgArray_addr[1],ImgArray_addr[2],ImgArray_addr[3]) 11 12 Imgfunction = f.py_img_address # 從當前庫當中取得clib中的函數py_struct_address,並重命名爲Sfunction 13 Imgfunction.argtypes = [POINTER(ctypes.c_ubyte), ctypes.c_uint32] # 設置當前函數的輸入參數規劃 usigned char *pointer, unsigned int arg 14 Imgfunction.restype = ctypes.c_int # 設置當前函數返回值參數爲 int 類型 15 time_start = time.time() 16 Res = Imgfunction(ImgArray_addr, ImgL) # 調用Imgfunction函數 17 time_end = time.time() 18 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 記錄當前函數調用消耗的時間 19 20 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,0]) # 打印0通道的數據 21 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,1]) # 打印1通道的數據 22 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,2]) # 答應2通道的數據
通過numpy庫中數組的成員 ctypes.data_as 方法獲取指定類型的指針地址,並將此地址應用在函數庫的參數中。
5. 基本類型定義
測試代碼:
1 # 使用byref獲取ctypes類型數據的地址 2 data = ctypes.c_uint8(10) # 定義一個整數類型的變量,變量初始值爲 10,相當於C語言中的 char data=42; 3 data_addr = ctypes.byref(data, 0) # 通過使用byref接口獲取地址,相當於C語言中的 char *data_addr = &data; byref(obj, offset) 對應於這段 C 代碼:(((char *)&obj) + offset) 4 print('Line', sys._getframe().f_lineno, ':', type(data)) 5 print('Line', sys._getframe().f_lineno, ':', type(data_addr))
這裏使用 ctypes.c_uint8(10) 定義一個 unsigned char 類型的數據並賦予初值爲10,byref函數接口用於獲取數據data的地址。
I) 完整的測試代碼:
MemoryTest.py
1 import os 2 import sys 3 import time 4 import ctypes 5 from ctypes import * 6 from ctypes import cdll 7 import numpy as np 8 9 BUFF_SIZE = 6*1024*1024 # 基本的緩衝區域尺寸大小定義 10 LENGTH = 2*2*3 # 數據長度定義 11 12 # 類定義 13 class POINT(Structure): # 定義了一個類,類當中的基本成員變量包括了x、y, 相當於C語言中的 struct POINT{int x;inty}; 14 _fields_ = [("x", c_int),("y", c_int),("addr", POINTER(c_uint8)),("len", c_int)] 15 16 # 特殊ctypes類型數組類型定義 17 ArrayTestType = ctypes.c_uint8 * 10 # 重定義數據類型 18 ArrayTest = ArrayTestType(11) # 定義數據並初始化數據賦值爲11 19 ArrayTest[5] = 20 # 對數據進行賦值處理 20 print('Line', sys._getframe().f_lineno, ':', ArrayTest, type(ArrayTest), ArrayTestType, type(ArrayTestType)) 21 print('Line', sys._getframe().f_lineno, ':', ArrayTest[0], ArrayTest[5]) 22 23 DataPoint = (c_uint8 * LENGTH)() # 定義一個大小爲10指針實例作爲緩存, 等效爲 Step1:DataType = c_uint8 * LENGTH --> Step2:DataPoint = DataType() 24 DataPoint[3] = 22 25 26 point = POINT(10, 20, DataPoint,LENGTH) # 定義個類對象Obj, 相當於 struct POINT point={10,20}; 27 print('Line', sys._getframe().f_lineno, ':', point.x, point.y, point.addr[3], point.len) 28 29 point = POINT(y=5) # 定義個類對象Obj, 相當於 struct POINT point={,20}; 30 print('Line', sys._getframe().f_lineno, ':', point.x, point.y) 31 32 # Class類數組定義 33 addrTest = (POINT * 1)() # 定義一個POINT結構體緩衝地址 34 addrTest[0].x = 10 # POINT緩衝地址的第一個變量的x成員值 35 addrTest[0].y = 11 # POINT緩衝地址的第一個變量的y成員值 36 addrTest[0].addr = DataPoint # POINT緩衝地址的第一個變量的addr成員賦值 37 addrTest[0].len = LENGTH # POINT緩衝地址的第一個變量的len成員賦值 38 print('Line', sys._getframe().f_lineno, ':', addrTest, addrTest[0].x,addrTest[0].y,addrTest[0].addr[3],addrTest[0].len) 39 40 TenPointsArrayType = POINT * 3 # 重定義了一個POINT數組類型,相當於C語言中的 #define TenPointsArrayType POINT*3 41 arr = TenPointsArrayType() 42 for pt in arr: 43 print(pt.x, pt.y, pt.addr) 44 45 # 從C語言或者C++的動態庫當中加載C函數以調用 46 p = os.getcwd() + '/libfunc.so' # 獲取當前的動態庫的絕對路徑位置 47 f = cdll.LoadLibrary(p) # 使用LoadLibrary接口加載C語言動態庫 48 function = f.py_point_address # 從當前庫當中取得clib中的函數py_point_address,並重命名爲function 49 function.argtypes = [POINTER(c_byte)] # 設置當前函數的輸入參數 50 51 # Python Class類 到 C struct結構體 數據類型的傳輸轉換 52 Sfunction = f.py_struct_address # 從當前庫當中取得clib中的函數py_struct_address,並重命名爲Sfunction 53 Sfunction.argtypes = [POINTER(POINT), POINTER(ctypes.c_char)] # 設置當前函數的輸入參數 54 55 p = create_string_buffer(b"Hello World", 15) # create a 10 byte buffer 56 print('Line', sys._getframe().f_lineno, ':', p,sizeof(p), repr(p.raw)) 57 58 time_start = time.time() 59 Res = Sfunction(addrTest, p) 60 time_end = time.time() 61 for i in range(LENGTH): 62 print(addrTest[0].addr[i]) 63 64 print('Line', sys._getframe().f_lineno, ':', addrTest[0].x, addrTest[0].y, addrTest[0].len, addrTest[0].addr) 65 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 記錄當前函數調用消耗的時間 66 67 # Numpy 數據類型等相互轉換測試(將內存數據轉換值Python當中) 68 ImgW = 1669 # 圖像寬度 69 ImgH = 21 # 圖像高度 70 ImgC = 3 # 圖像通道數 71 ImgL = ImgW*ImgH*ImgC # 圖像總長度 72 # ImgArray = np.array([[0, 1], [2, 3]], dtype=np.uint8) 73 ImgArray = np.zeros((ImgW,ImgH,ImgC), dtype=np.ubyte) # 申請圖像總空間爲多維 zeros 矩陣 74 # ImgArray = np.array([1,2,3,4], dtype=np.int32) 75 # print(ImgArray) 76 # print(ImgArray.ctypes.data) 77 print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))) # 將numpy數組轉換爲地址表示方式 data_as 並打印數據類型 78 ImgArray_addr = ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) # 獲取的指針放置在ImgArray_addr變量當中 79 print('Line', sys._getframe().f_lineno, ':', ImgArray_addr[0],ImgArray_addr[1],ImgArray_addr[2],ImgArray_addr[3]) 80 # print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)).contents) 81 # print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)).contents) 82 # print(ImgArray.ctypes.shape) 83 # print(ImgArray.ctypes.strides) 84 85 Imgfunction = f.py_img_address # 從當前庫當中取得clib中的函數py_struct_address,並重命名爲Sfunction 86 Imgfunction.argtypes = [POINTER(ctypes.c_ubyte), ctypes.c_uint32] # 設置當前函數的輸入參數規劃 usigned char *pointer, unsigned int arg 87 Imgfunction.restype = ctypes.c_int # 設置當前函數返回值參數爲 int 類型 88 time_start = time.time() 89 Res = Imgfunction(ImgArray_addr, ImgL) # 調用Imgfunction函數 90 time_end = time.time() 91 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 記錄當前函數調用消耗的時間 92 93 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,0]) # 打印0通道的數據 94 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,1]) # 打印1通道的數據 95 print('Line', sys._getframe().f_lineno, ':\n', ImgArray[:,:,2]) # 答應2通道的數據 96 97 # 一般類型定義以及數據取地址方法 int *pi = &i; 98 i = ctypes.c_int(42) # 定義一個整數類型的變量,變量初始值爲 42,相當於C語言中的 int i=42; 99 pi = ctypes.pointer(i) # 通過使用pointer接口獲取,相當於C語言中的 int *pi = &i; 100 print('Line', sys._getframe().f_lineno, ':', pi.contents) # 查看指針變量的信息 101 print('Line', sys._getframe().f_lineno, ':', pi[0]) # 查看指針所指向的內容,相當於C語言中的 *pi; 102 103 # ctypes.c_byte 類型的數組定義,等效於 byte a[BUFF_SIZE]; 104 a = (c_byte * BUFF_SIZE)() # 定義一個大小爲BUFF_SIZE指針實例作爲緩存 105 # cast(a, POINTER(c_uint8)) # 函數可以將一個指針實例強制轉換爲另一種 ctypes 類型 106 print('Line', sys._getframe().f_lineno, ':', a) 107 print('Line', sys._getframe().f_lineno, ':', type(a)) 108 time_start = time.time() 109 function(a) # 執行function函數並傳入a地址參數 110 time_end = time.time() 111 112 for i in range(10): 113 print(a[i]) 114 115 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 記錄當前函數調用消耗的時間 116 117 # 使用byref獲取ctypes類型數據的地址 118 data = ctypes.c_uint8(10) # 定義一個整數類型的變量,變量初始值爲 10,相當於C語言中的 char data=42; 119 data_addr = ctypes.byref(data, 0) # 通過使用byref接口獲取地址,相當於C語言中的 char *data_addr = &data; byref(obj, offset) 對應於這段 C 代碼:(((char *)&obj) + offset) 120 print('Line', sys._getframe().f_lineno, ':', type(data)) 121 print('Line', sys._getframe().f_lineno, ':', type(data_addr)) 122 123 # 使用id獲取變量在python的地址 124 value = 'hello world' # 定義一個字符串變量 125 address = id(value) # 獲取value的地址,賦給address 126 get_value = ctypes.cast(address, ctypes.py_object).value # 讀取地址中的變量 127 print('Line', sys._getframe().f_lineno, ':', address, get_value) # 128 129 # 一般Clib函數的調用 130 res = f.func(99) # 普通函數調用 131 print('Line', sys._getframe().f_lineno, ':', res)
function.c
1 #include <stdio.h> 2 #include <sys/shm.h> 3 #include <string.h> 4 #include <stdlib.h> 5 #include <time.h> 6 #include <sys/time.h> 7 8 9 #define BUFF_SIZE 6*1024*1024 10 11 typedef struct T_POINT{ 12 int x; 13 int y; 14 char * addr; 15 int len; 16 }POINT; 17 18 time_t get_timestamp_us(void) 19 { 20 time_t timestamp_ms = 0; 21 struct timeval tv; 22 23 gettimeofday(&tv,NULL); 24 timestamp_ms = tv.tv_sec * 1000 * 1000 + tv.tv_usec; 25 return timestamp_ms; 26 } 27 28 char *file_read(unsigned long *file_bytes, char *file_name) 29 { 30 int file_size; 31 FILE *fd = NULL; 32 char *file_data = NULL; 33 fd = fopen(file_name, "rw"); 34 if(fd < 0) 35 { 36 printf("File open failed...\n"); 37 return NULL; 38 } 39 fseek(fd, 0, SEEK_END); 40 file_size = ftell (fd); 41 file_data = malloc(sizeof(char)*file_size); 42 if(file_data == NULL) 43 { 44 printf("Malloc failed...\n"); 45 return NULL; 46 } 47 fseek(fd, 0, SEEK_SET); 48 *file_bytes = fread(file_data,sizeof(char),file_size,fd); 49 fclose(fd); 50 return file_data; 51 } 52 53 /* func.c */ 54 int func(int a) 55 { 56 return a*a; 57 } 58 59 void cycle_calc(int b) 60 { 61 int count = 100; 62 while(count--){ 63 b*=2; 64 printf("%d - %d\n", count, b); 65 } 66 } 67 68 unsigned char * c_point_address(void) 69 { 70 unsigned char *Img = malloc(sizeof(unsigned char)*1000); 71 printf("C-Address:%hhn\n", Img); 72 memset(Img, 20, 1000); 73 return Img; 74 } 75 76 int py_point_address(unsigned char * Addr) 77 { 78 unsigned char *Img = malloc(sizeof(unsigned char)*BUFF_SIZE); 79 // printf("C-Address:%x\n", Img); 80 // printf("Python-Address:%x\n", Addr); 81 memset(Img, 20, BUFF_SIZE); 82 memcpy((unsigned char * )Addr, Img, BUFF_SIZE); 83 return 1; 84 } 85 86 int py_struct_address(POINT *pt_POINT, char *str) 87 { 88 int i = 0; 89 for(i=0; i < pt_POINT->len; i++) 90 { 91 pt_POINT->addr[i] = i; 92 } 93 pt_POINT->x = 16; 94 pt_POINT->y = 17; 95 printf("FunctionPrint:%s\n",str); 96 return 1; 97 } 98 99 int py_img_address(unsigned char *data, unsigned int lenght) 100 { 101 time_t st,et; 102 unsigned long count=0; 103 unsigned long i=0; 104 st = get_timestamp_us(); 105 unsigned char *ImgData = file_read(&i, "./Test.PNG"); 106 et = get_timestamp_us(); 107 printf("C ### ReadFile time Cost:%ld\n", et - st); 108 109 printf("Data[%ld]:%d\n", count, *(ImgData+i-1)); 110 111 st = get_timestamp_us(); 112 memcpy(data, ImgData, sizeof(unsigned char)*lenght); 113 et = get_timestamp_us(); 114 printf("C ### Memcpy time Cost:%ld\n", et - st); 115 116 printf("py_img_address FunctionPrint:%ld\n", i); 117 return 1; 118 }
exe_shell.sh
1 #!/bin/bash 2 gcc -fPIC -shared function.c -o libfunc.so 3 python3 MemoryTest.py
II) 測試結果如下(測試平臺: Ubuntu 18.04.6 LTS + Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz ):
Line 91 : TimeCost: 0.07772445678710938 ms 102KB Speed=1.25GB/s
二、採用共享內存方式進行IPC通信
內存共享基本方法參考 《進程間通信原理》 ,共享內存的方式需要通過其他通信方式進行進程間數據同步,從而保證共享內存在使用的過程中不被其他進程修改。
main.py
1 from ctypes import * 2 import numpy as np 3 import codecs 4 import datetime 5 6 SHM_SIZE = 1024*1024*20 # 20MBytes 7 SHM_KEY = 123559 8 9 OUTFILE="Shared.PNG" 10 try: 11 rt = CDLL('librt.so') 12 except: 13 rt = CDLL('librt.so.1') 14 15 shmget = rt.shmget 16 shmget.argtypes = [c_int, c_size_t, c_int] 17 shmget.restype = c_int 18 shmat = rt.shmat 19 shmat.argtypes = [c_int, POINTER(c_void_p), c_int] 20 shmat.restype = c_void_p 21 22 shmid = shmget(SHM_KEY, SHM_SIZE, 0o666) 23 24 if shmid < 0: 25 print ("System not infected") 26 else: 27 addr = shmat(shmid, None, 0) 28 f=open(OUTFILE, 'wb') 29 begin_time = datetime.datetime.now() 30 DataLength = int.from_bytes(string_at(addr,4), byteorder='little', signed=True) #這裏數據文件是小端int16類型 31 ImgData = string_at(addr+4,DataLength) 32 end_time = datetime.datetime.now() 33 print(DataLength, ' Bytes') 34 print('Type:',type(ImgData),' Bytes:', len(ImgData)) 35 f.write(ImgData) 36 f.close() 37 #print ("Dumped %d bytes in %s" % (SHM_SIZE, OUTFILE)) 38 print("Success!",end_time-begin_time)
main.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/shm.h> 4 #include <string.h> 5 #include <time.h> 6 #include <sys/time.h> 7 8 #define SHAERD_MEM_SIZE 20 * 1024 * 1024 // 20MBytes 9 10 char mem_free(void *ptr); 11 time_t get_timestamp_ms(void); 12 char *file_read(unsigned int *file_bytes, char *file_name); 13 14 int main(int argc, char *argv[]) 15 { 16 int id = 0; 17 size_t offset = 0; 18 char *data = NULL; 19 char *ImgData = NULL; 20 unsigned int file_bytes = 0; 21 time_t start_time, end_time; 22 if (argc < 2) 23 { 24 printf("args too less\n"); 25 return 0; 26 } 27 28 id = shmget(123559, SHAERD_MEM_SIZE, IPC_CREAT | 0777); 29 if (id < 0) 30 { 31 printf("get id failed\n"); 32 return 0; 33 } 34 35 data = shmat(id, NULL, 0); 36 if (data == NULL) 37 { 38 printf("shamt failed\n"); 39 return 0; 40 } 41 42 ImgData = file_read(&file_bytes, argv[1]); 43 offset = sizeof(unsigned int); 44 printf("Size of unsigned long:%d\n", offset); 45 printf("Size of Image File:%d\n", file_bytes); 46 start_time = get_timestamp_ms(); 47 memcpy(data, &file_bytes, sizeof(unsigned int)); 48 memcpy(data + offset, ImgData, file_bytes); 49 end_time = get_timestamp_ms(); 50 51 printf("Time Cost:%d\n", end_time - start_time); 52 53 mem_free(ImgData); 54 55 return 0; 56 } 57 58 char *file_read(unsigned int *file_bytes, char *file_name) 59 { 60 int file_size; 61 FILE *fd = NULL; 62 char *file_data = NULL; 63 fd = fopen(file_name, "rw"); 64 if(fd < 0) 65 { 66 printf("File open failed...\n"); 67 return NULL; 68 } 69 fseek(fd, 0, SEEK_END); 70 file_size = ftell (fd); 71 file_data = malloc(sizeof(char)*file_size); 72 if(file_data == NULL) 73 { 74 printf("Malloc failed...\n"); 75 return NULL; 76 } 77 fseek(fd, 0, SEEK_SET); 78 *file_bytes = fread(file_data,sizeof(char),file_size,fd); 79 fclose(fd); 80 return file_data; 81 } 82 83 time_t get_timestamp_ms(void) 84 { 85 time_t timestamp_ms = 0; 86 struct timeval tv; 87 88 gettimeofday(&tv,NULL); 89 timestamp_ms = tv.tv_sec * 1000 + tv.tv_usec / 1000; 90 return timestamp_ms; 91 } 92 93 char mem_free(void *ptr) 94 { 95 if(NULL != ptr) 96 { 97 free(ptr); 98 return 0; 99 } 100 printf("Memory is Empty...\n"); 101 return -1; 102 }
編譯執行即可
gcc -o main main.c ./main sdlinux.zip # 12MB python3 main.py
測試結果如下(測試平臺:Ubuntu 18.04.6 LTS + Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz ):
Success! 7.091045379638672 ms 12MB Speed=1.67GB/s
Reference:
REF1:https://zhuanlan.zhihu.com/p/145165873
未完待續 ~