热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

内存和IO访问

内存的概念在Linux系统中相对复杂,有常规内存、高端内存、虚拟地址、逻辑地址、总线地址、物理地址、IO内存、设备内存、预留内存等概念。本章将系统讲解内存和IO访问编程CPU和内存、IO

内存的概念在Linux系统中相对复杂,有常规内存、高端内存、虚拟地址、逻辑地址、总线地址、物理地址、IO内存、设备内存、预留内存等概念。本章将系统讲解内存和IO访问编程

CPU和内存、IO

内存空间和IO空间

在X86处理器中存在着IO空间的概念,IO空间是相对于内存空间而言的,它通过特定指令in和out来访问。
端口号标志了外设的寄存器地址。
目前,大部分的嵌入式微控制器(如ARM、PowerPC等)中并不提供IO空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序及在程序运行中使用的变量和其他数据都存在与内存空间中。
内存地址可以直接由C语言指针操作:e.g:

unsigned char*p = (unsigned char *) 0xF000FF00;
*p = 11;

以上程序的意思是在绝对地址0xF000 + 0xFF00(186处理器使用16位短地址和16位偏移地址)中写入11
而在ARM、PowerPC等未采用段地址的处理器中,p指向的内存空间就是0xF000FF00,而*p = 11,就是在该地址写入11
内存空间是必须的,IO空间是可选的

内存管理单元

高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。操作系统内核借助MMU可以让用户感觉到程序好像可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中物理内存的实际容量。
为了理解基本的MMU操作原理,先明晰几个概念:
1.TLB:即转换旁路缓存(Translation Lookaside Buffer):即转换旁路缓存,TLB是MMU的核心部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,因此也经常被称为快表。
2.TTW: 即转换表漫游(Translation Table walk),当TLB中没有缓存对应的地址转换关系的时候,需要通过对内存中转换表(多级页表)的访问来获得虚拟地址与物理地址的对应关系。TTW成功后,结果应该写入TLB中
3.要么TLB成功,要么TTW后把信息写入TLB,然后查找转换关系成功,反正最后都是会TLB成功的。除非该虚拟地址与物理地址的对应关系即不在TLB中也不在内存中的转换表中。
4.ARM中TLB条目中的控制信息用于控制对对应地址的访问权限以及Cache的操作地址。

  • C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否进行高速缓存
  • 访问权限和域位用来控制读写访问是否被允许。如果不被允许,MMU则向ARM处理器发送一个存储器异常,否则访问将被允许进行

上述的MMU机制针对的虽然是ARM处理器,但是PowerPC、MIPS等其他处理器也有类似的操作
MMU有虚拟地址和物理地址转换、内存访问权限保护等功能,这将使得Linux操作系统能单独为系统每个用户进程分配独立的内存空间并保证用户空间不能访问内核空间的地址,为操作系统的内存管理模块提供硬件基础。

内存管理单元

对于包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。
在Linux系统中,进程的4GB内存空间被分为两个部分–用户空间和内核空间。用户空间的地址一般分布为0-3GB,这样剩下3GB~4GB为内核空间。用户进程通常是访问不到内核空间的地址的,只有当系统调用(陷入到内核态时)才可以访问到内核空间
每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是有内核负责映射,它不会跟着进程改变,是固定的。内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其他进程。
Linux中1GB的内核地址空间,由3GB~4GB,又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区。
当系统物理内存超过4GB时,必须使用CPU的扩展分页模式所提供的64位页目录项才能存取到4GB以上的物理内存,这需要CPU的支持。
直接映射的896MB物理内存其实又分为两个区域,在低于16MB的区域,ISA设备可以做DMA,所以该区域是DMA区域。16M~896M之间的为常规区域,高于896MB的称为高端内存区域。(x86)
32位ARM Linux的内核空间地址映射与x86不一样,地址由低到高分别是向量表、vmalloc、DMA+常规区域内存映射、高端内存映射区、内核模块。
DMA、常规、高端内存这3个区域都是采用buddy算法进行管理,把空闲的页面以2的n次方为单位进行管理。Buddy算法最主要的优点是避免了外部碎片,任何时候区域里的空闲空间都可以以2的n次方进行拆分或者合并。
cat /proc/buddyinfo 会显示每个区域里面2的n次方的空闲页面分布情况。如下
Node 0, zone DMA 8 5 2 7 8 3 0 0
Node 0, zone Normal 2002 1252 524 187
上述结果表示:
高端内存区域为0,DMA区域有一页空闲的内存有8个,连续2页空闲的内存有5,4页的有2个
常规内存区域里面有一页空闲的内存有2002个,连续两页空闲的有1252个,。。。。
在DMA和常规内存区域里面,有一种简单的方式换算虚拟地址和物理地址。
virt_to_phys()和phys_to_virt()

内存存取

用户空间内存动态申请

在用户空间中动态申请内存的函数为malloc(),释放函数是free(),Linux内核总是采用按需调页,因此当malloc返回的时候,虽然都是成功返回,但是内核并没有真正的给这个进程内存。如果去读这个申请的内存,全部都是0,这个页面的映射是只读的。只有当写到某个页面的时候,内核才在页错误之后把这个页面返回进程。

内核空间内存动态申请

Linux内核空间申请内存主要包括kmalloc()、__get_free_pages()和vmalloc等。kmalloc和__get_free_pages()(以及类似的函数)申请的内存位于DMA和常规区域的映射区,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmalloc在虚拟内存空间给出一块连续的内存区,实际上这块连续的虚拟地址在物理上不一定是连续的,而且物理地址与虚拟地址之间也没有简单的换算关系。
1. kmalloc

void *kmalloc(size_t size, int flags);

size:要分配的块的大小
flags:分配标志,用于控制kmalloc的行为,最常见的是GFP_KERNEL,其含义是在内核空间的进程中申请内存。
kmalloc的底层依赖于__get_free_pages()来实现,分配标志的GFP也是该函数的缩写。用GFP_KERNEL来申请内存的时候,若暂时不能满足,则进程会睡眠等待页,就是引起阻塞,所以不能再中断上下文或者持有自旋锁的时候使用GFP_KERNEL标志来申请内存。
由于在中断处理函数、tasklet、内核定时器等非进程上下文中不能阻塞,所以此时应该使用GFP_ATOMIC标志来申请内存。当使用GFP_ATOMIC标志申请内存的时候,若不存在空闲页,则不等待直接返回。
常用的就是这两个标志位,kfree是相应的内存释放函数。
2.__get_free_pages
__get_free_pages系列函数/宏本质上是Linux内核最底层用于获取空闲内存的方法,因此底层的buddy算法以2的n次方页为单位管理空闲内存,所以最底层的内存申请总是以2的n次方为单位的
3.vmalloc
vmalloc一般用于较大内存的分配,vfree是对应的内存释放函数,vmalloc在申请内存的时候,会进行内存的映射,改变页表项。它的虚拟地址和实际物理地址并不是一个简单的线性映射关系
4.slab和内存池
一方面,完全使用页为单元申请和释放内存容易导致浪费;另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。如果我们能用合适的方法使得对象在前后两次被使用时分配在同一块内存或者同一类内存空间且保留了基本的数据结构,就可以提高效率。于是slab算法应运而生,kmalloc就是使用slab的机制实现的。
slab建立在buddy算法之上,它从buddy算法中拿到2的n次方页面后再次进行二次管理,这一点和用户空间的c库很像。slab申请的内存以及基于slab的kmalloc申请的内存地址,与实际物理内存地址之间存在一个简单的线性偏移
slab在最底层仍然依赖__get_free_pages(),slab在底层每次申请1页或者多页,之后再分隔这些页为更小的单元进行管理。
Linux内核还包含了内存池的支持,内存池技术也是一种经典的用于分配大量小对象的后背缓存技术
1.创建内存池

mempoll_t *mempool_create(int min_nr, mempoll_alloc_t *alloc_fn, mempoll_free_t *free_fn, void *poll_data);

该函数用来创建一个内存池,min_nr参数是需要预分配对象的数目,alloc_fn和free_fn是内存池提供的对象分配和回收函数指针,分别如下

typedef void *(mempoll_alloc_t)(int gfp_mask, void *poll_data);
typedef void (mempoll_free_t)(void *element, void *poll_data);

2.分配和回收内存池

void *mempoll_alloc(mempoll_t *poll, int gfp_msak);
void mempoll_free(void *element, mempoll_t *poll);

mempoll_alloc用来分配对象,如果内存池分配器无法提供内存,那么就可以用预分配的内存池
3.回收内存池

void mempoll_destory(mempoll_t *poll);

mempoll_create创建的内存池需要由mempoll_destory回收。

设备IO端口和IO内存访问

设备通常会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器、状态寄存器。这些寄存器可能位于IO空间中,也可能位于内存空间中。当位于IO空间(大部分处理器并没有这个东西)中的时候,通常被称为IO端口;当位于内存空间的时候,被称为IO内存

LinuxIO端口和IO内存访问接口

  1. IO端口
    在Linux设备驱动中,应使用Linux内核提供的函数来访问定位于IO空间的端口
  2. IO内存
    在内核中访问IO内存(通常是芯片内部的各个I2C、SPI、USB等控制器的寄存器或者外部内存总线上的设备)之前,需首先使用ioremap函数将设备所处的物理地址映射虚拟地址上。ioremap的原型如下:

    void *ioremap(unsigned long offset, unsigned long size);
    

    ioremap与vmalloc类似,也需要建立新的页表,但是它并不进行vmalloc中所执行的内存分配行为。ioremap返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc映射区域。通过ioremap()获得的虚拟地址应该被iounmap()函数释放,其原型如下:

    void iounmap(void *addr);
    

    ioremap有个变体是devm_ioremap,类似于其他以devm_开头的函数,通过devm_ioremap进行的映射通常是不需要在驱动退出和出错处理的时候进行iounmap()。
    在设备的物理地址一般都是寄存器被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是Linux内核推荐使用标准的API来完成设备内存映射的虚拟地址的读写。

申请和释放设备的IO端口和IO内存

  1. IO端口申请
    Linux内核提供了一组函数以申请和释放IO端口,表明该驱动要访问这片区域。

    struct resource *request_region(unsigned long first, unsigned long n, const char *name);
    

    这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名称。如果申请失败返回NULL

    void release_region(unsigned long start, unsigned long n);
    

    2.IO内存申请
    同样的,Linux内核也提供了一组函数申请和释放IO内存的范围。此处的“申请”表明驱动要访问这块区域,不会做任何内存映射的动作

    struct resource *request_mem_region(unsigned long start, unsigned long len, char* name)
    void release_men_region(unsigned long start, unsigned long len);
    

    request_region和request_mem_region都有对应的devm版本,不需要释放的版本

设备IO端口和IO内存访问流程

设备驱动访问IO端口和IO内存的步骤如下:
IO端口访问的途径:直接使用IO端口操作函数,在设备打开或者驱动模块被加载时申请IO端口区域,之后使用inb()/outb()等进行端口访问,最后在设备关闭或者驱动被卸载的时候释放IO端口区域。
IO内存的访问步骤:首先调用request_mem_region申请资源,接着将寄存器地址通过ioremap映射到内核空间虚拟地址,之后就可以通过Linux设备访问编程接口来访问这些设备的寄存器。访问完成后,调用iounmap释放申请的虚拟地址,调用release_mem_region申请IO内存资源。

将设备地址映射到用户空间

1.内存映射与VMA
一般情况下,用户空间不可能也不应该直接访问设备的。但是设备驱动程序实现了mmap()函数,使得用户空间能直接访问设备的物理地址。实际上,mmap()函数实现了这样一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间这段地址范围时,实际上会转化为对设备的访问。
这种能力对于显示适配器一类的设备非常有意义,如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点像素将不再需要一个从用户空间到内核空间的复制的过程。
mmap()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。
从file_operation文件操作结构体可以看出,驱动中mmap()函数的原型如下:

int(*mmap)(struct file*, struct vm_area_struct*);

驱动中的mmap()函数将在用户进行mmap系统调用时,最终被调用到。系统调用的mmap函数原型如下:

cadrr_t mmap(cadrr_t addr, size_t len, int prot, int flags, int fd, off_t offset);

参数fd为文件描述符,一般由open返回,fd也可以返回-1,此时需制定flags参数中的MAP_ANON,表明进行的是匿名映射。
len是映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起,offset参数一般设为0,表示从文件头开始映射。
port参数制定访问权限,可取如下几个值得或:可读、可写、可执行、不可访问
参数addr指定文件应被映射到用户空间的起始地址,一般被指定为NULL,这样,选择起始地址的任务将由内核完成,而参数的返回值就是映射到用户空间的地址。其类型caddr_t实际上就是void*
当用户调用mmap的时候,内核会进行如下处理。

1.在进程的虚拟空间查找一块VMA(虚拟内存区域)
2.将这块VMA进行映射
3.如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它
4.将这个VMA插入进程的VMA链表中
file_operations中mmap函数的第一个参数就是步骤1中找到的VMA
由mmap系统调用映射的内存可有munmap接触映射,函数原型如下:

int munmap(caddr_t addr, size_t len);

驱动程序中mmap的实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。VMA就是vm_area_struct,用于描述一个虚拟的内存区域,VMA结构体的定义如下:

struct vm_area_struct {
unsigned long vm_start;
unsigned long vm_end;
...
const struct vm_operations_struct *vm_ops;
...
}

VMA结构体描述的虚地址介于vm_start和vm_end之间,其vm_ops成员指向这个VMA的操作集。针对VMA的操作都被包含在vm_operations_struct结构体重,vm_operations_struct结构体的定义如下:

struct vm_operations_struct {
    void (*open) (struct vm_area_struct *area);
    void (*close) (struct vm_area_struct *area);
    ....
}

整个vm_operations_struct 结构体的实体会在file_operations的mmap成员函数里被复制给相应的vma->vm_ops,而上述的open函数也通常会在mmap函数里调用,close函数也会在用户调用munmap的时候被调用到。vm_operations_struct的操作范例:

static int xxx_mmap(struct file*filp, struct vm_area_struct *vma)
{
    if(remap_pfn_range(vma, vma->start, vma->pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot))    //建立页表
    return -EAGAIN;
    vma->vm_ops = &xxx_remap_vm_ops;
    xxx_vma_open(vma);
    return 0;
}

static void xxx_vma_open(struct vm_area_struct *vma)  //vma打开函数
{
    ...
}

static void xxx_vma_close(struct vm_area_struct *vma)  //vma关闭函数
{
    ...
}

static struct vm_operations_struct xxx_remap_vm_ops ={
    .open = xxx_vma_open,
    .close = xxx_vma_close,
    ...
}

调用remap_pfn_range()创建页表项,以VMA结构体的成员(VMA的数据成员是内核根据用户自己的请求填充的)作为remap_pfn_range()的参数,映射的虚拟地址范围是vma->vm_start至vma->end。
remap_pfn_range()函数的原型如下:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot)

其中addr参数表示内存映射开始处的虚拟地址。remap_pfn_range函数为addr~addr+size的虚拟地址构造页表。
pfn是虚拟地址应该映射到的物理地址的页帧,实际上就是物理地址右移PAGE_SHIFT位,若PAGE_SIZE位4KB (==2的12次方),所以PAGE_SHIFT为12。
prot是新页所要求的保护属性
在驱动程序中,我们能使用remap_pfn_range()映射内存中的保留页、设备IO、framebuffer、camera等内存。在remap_pfn_range()上又可以进一步封装出io_remap_pfn_range、vm_iomap_memory等API

fault函数

VMA的fault函数通常可以为设备提供更加灵活的内存映射途径。当访问页不在内存里,即发生缺页异常时,fault会被内核自动调用,而fault的具体行为可以自定义。这是因为当发生缺页异常时,系统会经过如下的处理过程:
1. 找到缺页的虚拟地址所在的VMA
2. 如果必要,分配中间页目录表和页表。
3. 如果页表项对应的物理页面不存在,则调用这个VMA的fault方法,它返回物理页面的页描述符
4. 讲物理的页面地址填充到页表中
fault的典型范例:

static int xxx_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
    unsigned long paddr;
    unsigned long pfn;
    pgoff_t index = vmf->pgoff;
    struct vma_data *vdata = vma->vm_private_data;
    ...
    pfn = paddr >> PAGE_SHIFT;
    vm_insert_pfn(vma, (unsigned long)vmf->virtual_address, pfn);
    return VM_FAULT_NOPAGE;
}

推荐阅读
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了在Ubuntu 11.10 x64环境下安装Android开发环境的步骤,并提供了解决常见问题的方法。其中包括安装Eclipse的ADT插件、解决缺少GEF插件的问题以及解决无法找到'userdata.img'文件的问题。此外,还提供了相关插件和系统镜像的下载链接。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Python语法上的区别及注意事项
    本文介绍了Python2x和Python3x在语法上的区别,包括print语句的变化、除法运算结果的不同、raw_input函数的替代、class写法的变化等。同时还介绍了Python脚本的解释程序的指定方法,以及在不同版本的Python中如何执行脚本。对于想要学习Python的人来说,本文提供了一些注意事项和技巧。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了Linux系统中正则表达式的基础知识,包括正则表达式的简介、字符分类、普通字符和元字符的区别,以及在学习过程中需要注意的事项。同时提醒读者要注意正则表达式与通配符的区别,并给出了使用正则表达式时的一些建议。本文适合初学者了解Linux系统中的正则表达式,并提供了学习的参考资料。 ... [详细]
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • 恶意软件分析的最佳编程语言及其应用
    本文介绍了学习恶意软件分析和逆向工程领域时最适合的编程语言,并重点讨论了Python的优点。Python是一种解释型、多用途的语言,具有可读性高、可快速开发、易于学习的特点。作者分享了在本地恶意软件分析中使用Python的经验,包括快速复制恶意软件组件以更好地理解其工作。此外,作者还提到了Python的跨平台优势,使得在不同操作系统上运行代码变得更加方便。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 本文介绍了GTK+中的GObject对象系统,该系统是基于GLib和C语言完成的面向对象的框架,提供了灵活、可扩展且易于映射到其他语言的特性。其中最重要的是GType,它是GLib运行时类型认证和管理系统的基础,通过注册和管理基本数据类型、用户定义对象和界面类型来实现对象的继承。文章详细解释了GObject系统中对象的三个部分:唯一的ID标识、类结构和实例结构。 ... [详细]
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • 本文介绍了利用ARMA模型对平稳非白噪声序列进行建模的步骤及代码实现。首先对观察值序列进行样本自相关系数和样本偏自相关系数的计算,然后根据这些系数的性质选择适当的ARMA模型进行拟合,并估计模型中的位置参数。接着进行模型的有效性检验,如果不通过则重新选择模型再拟合,如果通过则进行模型优化。最后利用拟合模型预测序列的未来走势。文章还介绍了绘制时序图、平稳性检验、白噪声检验、确定ARMA阶数和预测未来走势的代码实现。 ... [详细]
  • 如何使用PLEX播放组播、抓取信号源以及设置路由器
    本文介绍了如何使用PLEX播放组播、抓取信号源以及设置路由器。通过使用xTeve软件和M3U源,用户可以在PLEX上实现直播功能,并且可以自动匹配EPG信息和定时录制节目。同时,本文还提供了从华为itv盒子提取组播地址的方法以及如何在ASUS固件路由器上设置IPTV。在使用PLEX之前,建议先使用VLC测试是否可以正常播放UDPXY转发的iptv流。最后,本文还介绍了docker版xTeve的设置方法。 ... [详细]
author-avatar
Cyndi丶先生
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有