Linux块设备IO子系统

   

块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,块设备(blockdevice)是一种具有一定结构的随机存取设备,对这种设备的读写是按(所以叫块设备)进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。作为存储设备,块设备驱动的核心问题就是哪些page->segment->block->sector与哪些sector有数据交互,本文以3.14为蓝本,探讨内核中的块设备驱动模型。

#框架

下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一个块设备:

/dev和文件系统挂载点,前者和字符设备一样,通常用于配置,后者就是我们mount之后通过文件系统直接访问一个块设备了。

  1. read()系统调用最终会调用一个适当的VFS函数(read()-->sys_read()-->vfs_read()),将文件描述符fd和文件内的偏移量offset传递给它。

  2. VFS会判断这个SCI的处理方式,如果访问的内容已经被缓存在RAM中(磁盘高速缓存机制),就直接访问,否则从磁盘中读取

  3. 为了从物理磁盘中读取,内核依赖映射层mapping layer,即上图中的磁盘文件系统

    1. 确定该文件所在文件系统的块的大小,并根据文件块的大小计算所请求数据的长度。本质上,文件被拆成很多块,因此内核需要确定请求数据所在的块

    2. 映射层调用一个具体的文件系统的函数,这个层的函数会访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。

  4. 内核利用通用块层(generic block layer)启动IO操作来传达所请求的数据,通常,一个IO操作只针对磁盘上一组连续的块。

  5. IO调度程序根据预先定义的内核策略将待处理的IO进行重排和合并

  6. 块设备驱动程序向磁盘控制器硬件接口发送适当的指令,进行实际的数据操作

#块设备 VS 字符设备

作为一种存储设备,和字符设备相比,块设备有以下几种不同:

字符设备块设备
1byte块,硬件块各有不同,但是内核都使用512byte描述
顺序访问随机访问
没有缓存,实时操作有缓存,不是实时操作
一般提供接口给应用层块设备一般提供接口给文件系统
是被用户程序调用由文件系统程序调用

此外,大多数情况下,磁盘控制器都是直接使用DMA方式进行数据传送。

#IO调度

就是电梯算法。我们知道,磁盘是的读写是通过机械性的移动磁头来实现读写的,理论上磁盘设备满足块设备的随机读写的要求,但是出于节约磁盘,提高效率的考虑,我们希望当磁头处于某一个位置的时候,一起将最近需要写在附近的数据写入,而不是这写一下。

IO调度就是将上层发下来的IO请求的顺序进行重新排序以及对多个请求进行合并,这样就可以实现上述的提高效率、节约磁盘的目的。这种解决问题的思路使用电梯算法,一个运行中的电梯,一个人从20楼->1楼,另外一个人从15->5楼,电梯不会先将第一个人送到1楼再去15楼接第二个人将其送到5楼,而是从20楼下来,到15楼的时候停下接人,到5楼将第二个放下,最后到达1楼。

一句话,电梯算法最终服务的优先顺序并不按照按按钮的先后顺序

Linux内核中提供了下面的几种电梯算法来实现IO调度:

  • No-op I/O scheduler只实现了简单的FIFO的,只进行最简单的合并,比较适合基于Flash的存储

  • Anticipatory I/O scheduler推迟IO请求(大约几个微秒),以期能对他们进行排序,获得更高效率

  • Deadline I/O scheduler试图把每次请求的延迟降到最低,同时也会对BIO重新排序,特别适用于读取较多的场合,比如数据库

  • CFQ I/O scheduler为系统内所有的任务分配均匀的IO带宽,提供一个公平的工作环境,在多媒体环境中,能保证音视频及时从磁盘中读取数据,是当前内核默认的调度器

我们可以通过内核传参的方式指定使用的调度算法

kernel elevator=deadline

或者,使用如下命令改变内核调度算法

echo SCHEDULER > /sys/block/DEVICE/queue/scheduler

#Page->Segment->Block->Sector  VS  Sector

VS左面的是数据交互中的内存部分。

Page就是内存映射的最小单位;
Segment就是一个Page中我们要操作的一部分,由若干个相邻的块组成;
Block是逻辑上的进行数据存取的最小单位,是文件系统的抽象,逻辑块的大小是在格式化的时候确定的, 一个 Block 最多仅能容纳一个文件(即不存在多个文件同一个block的情况)。如果一个文件比block小,他也会占用一个block,因而block中空余的空间会浪费掉。

而一个大文件,可以占多个甚至数十个成百上千万的block。Linux内核要求 Block_Size = Sector_Size  * (2的n次方),并且Block_Size <= 内存的Page_Size(页大小), 如ext2 fs的block缺省是4k。若block太大,则存取小文件时,有空间浪费的问题;若block太小,则硬盘的 Block 数目会大增,而造成 inode 在指向 block 的时候的一些搜寻时间的增加,又会造成大文件读写方面的效率较差,block是VFS和文件系统传送数据的基本单位。

block对应磁盘上的一个或多个相邻的扇区,而VFS将其看成是一个单一的数据单元,块设备的block的大小不是唯一的,创建一个磁盘文件系统时,管理员可以选择合适的扇区的大小,同一个磁盘的几个分区可以使用不同的块大小。此外,对块设备文件的每次读或写操作是一种"原始"访问,因为它绕过了磁盘文件系统,内核通过使用最大的块(4096)执行该操作。Linux对内存中的block会被进一步划分为Sector,Sector是硬件设备传送数据的基本单位,这个Sector就是512byte,和物理设备上的概念不一样,如果实际的设备的sector不是512byte,而是4096byte(eg SSD),那么只需要将多个内核sector对应一个设备sector即可

VS右边是物理上的概念,磁盘中一个Sector是512Byte,SSD中一个Sector是4K

#核心数据结构

gendisk是一个物理磁盘或分区在内核中的描述

block_device_operations描述磁盘的操作方法集,block_device_operations之于gendisk,类似于file_operations之于cdev

request_queue对象表示针对一个gendisk对象的所有请求的队列,是相应gendisk对象的一个域

request表示经过IO调度之后的针对一个gendisk(磁盘)的一个"请求",是request_queue的一个节点。多个request构成了一个request_queue

bio表示应用程序对一个gendisk(磁盘)原始的访问请求,一个bio由多个bio_vec,多个bio经过IO调度和合并之后可以形成一个request。

bio_vec描述的应用层准备读写一个gendisk(磁盘)时需要使用的内存页page的一部分,即上文中的"段",多个bio_vec和bio_iter形成一个bio

bvec_iter描述一个bio_vec中的一个sector信息

#核心方法

set_capacity()设置gendisk对应的磁盘的物理参数

blk_init_queue()分配+初始化+绑定一个有IO调度的gendisk的requst_queue,处理函数是void (request_fn_proc) (struct request_queue *q);类型

blk_alloc_queue() 分配+初始化一个没有IO调度的gendisk的request_queue,

blk_queue_make_request()绑定处理函数到一个没有IO调度的request_queue,处理函数函数是void (make_request_fn) (struct request_queue *q, struct bio *bio);类型

__rq_for_each_bio()遍历一个request中的所有的bio

bio_for_each_segment()遍历一个bio中所有的segment

rq_for_each_segment()遍历一个request中的所有的bio中的所有的segment
最后三个遍历算法都是用在request_queue绑定的处理函数中,这个函数负责对上层请求的处理。


#gendisk结构体

同样是面向对象的设计方法,Linux内核使用gendisk对象描述一个系统的中的块设备,类似于Windows系统中的磁盘分区和物理磁盘的关系,OS眼中的磁盘都是逻辑磁盘,也就是一个磁盘分区,一个物理磁盘可以对应多个磁盘分区,在Linux中,这个gendisk就是用来描述一个逻辑磁盘,也就是一个磁盘分区。

165 struct gendisk {
169         int major;                      /* major number of driver */
170         int first_minor;
171         int minors;
174         char disk_name[DISK_NAME_LEN];  /* name of major driver */
175         char *(*devnode)(struct gendisk *gd, umode_t *mode);
177         unsigned int events;            /* supported events */
178         unsigned int async_events;      /* async events, subset of all */
185         struct disk_part_tbl __rcu *part_tbl;
186         struct hd_struct part0;
188         const struct block_device_operations *fops;
189         struct request_queue *queue;
190         void *private_data;
192         int flags;
193         struct device *driverfs_dev;  // FIXME:
194         struct kobject *slave_dir;
196         struct timer_rand_state *random;
197         atomic_t sync_io;               /* RAID */
198         struct disk_events *ev;
200         struct blk_integrity *integrity;
202         int node_id;
203 };

struct gendisk解析

--169-->驱动的主设备号
--170-->第一个次设备号
--171-->次设备号的数量,即允许的最大分区的数量,1表示不允许分区
--174-->设备名称
--185-->分区表数组首地址
--186-->第一个分区,相当于part_tbl->part[0]
--188-->操作方法集指针
--189-->请求对象指针
--190-->私有数据指针
--193-->表示这是一个设备

gendisk是一个动态分配的结构体,所以不要自己手动来分配,而是使用内核相应的API来分配,其中会做一些初始化的工作

struct gendisk *alloc_disk(int minors);
void add_disk(struct gendisk *disk);
void del_gendisk(struct gendisk *gp);

上面几个API是一个块设备驱动中必不可少的部分,下面的两个主要是用来内核对于设备管理用的,通常不要驱动来实现

struct kobject *get_disk(struct gendisk *disk);void put_disk(struct gendisk *disk);

这两个API最终回调用kobject *get_disk() 和kobject_put()来实现对设备的引用计数

#block_device_operations结构体

和字符设备一样,如果使用/dev接口访问块设备,最终就会回调这个操作方法集的注册函数

1558 struct block_device_operations {
1559         int (*open) (struct block_device *, fmode_t);
1560         void (*release) (struct gendisk *, fmode_t);
1561         int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1562         int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1563         int (*direct_access) (struct block_device *, sector_t,
1564                                                 void **, unsigned long *);
1565         unsigned int (*check_events) (struct gendisk *disk,
1566                                       unsigned int clearing);
1568         int (*media_changed) (struct gendisk *);
1569         void (*unlock_native_capacity) (struct gendisk *);
1570         int (*revalidate_disk) (struct gendisk *);
1571         int (*getgeo)(struct block_device *, struct hd_geometry *);
1573         void (*swap_slot_free_notify) (struct block_device *, unsigned long);
1574         struct module *owner;
1575 };

struct block_device_operations

--1559-->当应用层打开一个块设备的时候被回调
--1560-->当应用层关闭一个块设备的时候被回调
--1562-->相当于file_operations里的compat_ioctl,不过块设备的ioctl包含大量的标准操作,所以在这个接口实现的操作很少
--1567-->在移动块设备中测试介质是否改变的方法,已经过时,同样的功能被check_event()实现
--1571-->即get geometry,获取驱动器的几何信息,获取到的信息会被填充在一个hd_geometry结构中
--1574-->模块所属,通常填THIS_MODULE

#request_queue结构体

每一个gendisk对象都有一个request_queue对象,前文说过,块设备有两种访问接口,一种是/dev下,一种是通过文件系统,后者经过IO调度在这个gendisk->request_queue上增加请求,最终回调与request_queue绑定的处理函数,将这些请求向下变成具体的硬件操作

 294 struct request_queue {298         struct list_head        queue_head;300         struct elevator_queue   *elevator;472 };
struct request_queue

--298-->请求队列的链表头
--300-->请求队列使用的IO调度算法, 通过内核启动参数来选择: kernel elevator=deadline
request_queue_t和gendisk一样需要使用内核API来分配并初始化,里面大量的成员不要直接操作, 此外, 请求队列如果要正常工作还需要绑定到一个处理函数中, 当请求队列不为空时, 处理函数会被回调, 这就是块设备驱动中处理请求的核心部分!

从驱动模型的角度来说, 块设备主要分为两类需要IO调度的和不需要IO调度的, 前者包括磁盘, 光盘等, 后者包括Flash, SD卡等, 为了保证模型的统一性 , Linux中对这两种使用同样的模型但是通过不同的API来完成上述的初始化绑定

#有IO调度类设备API

struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock
_t *lock)

#无IO调度类设备API

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)void blk_queue_make_request(struct request_queue *q, make_request_
fn *mfn)

#共用API

针对请求队列的操作是块设备的一个核心任务, 其实质就是对请求队列操作函数的编写, 这个函数的主要功能就是从请求队列中获取请求并根据请求进行相应的操作 内核中已经提供了大量的API供该函数使用

void blk_cleanup_queue(struct request_queue *q)
blkdev_dequeue_request()
struct request *blk_fetch_request(struct request_queue *q)
struct request *blk_peek_request(struct request_queue *q)
void blk_stop_queue(struct request_queue *q)
void blk_start_queue(struct request_queue *q)
  

#request

 97 struct request {98         struct list_head queuelist;104         struct request_queue *q;117         struct bio *bio;118         struct bio *biotail;119120         struct hlist_node hash;126         union {127                 struct rb_node rb_node;128                 void *completion_data;129         };137         union {138                 struct {139                         struct io_cq            *icq;140                         void                    *priv[2];141                 } elv;142143                 struct {144                         unsigned int            seq;145                         struct list_head        list;146                         rq_end_io_fn            *saved_end_io;147                 } flush;148         };149150         struct gendisk *rq_disk;151         struct hd_struct *part;199 };

struct request
--98-->将这个request挂接到链表的节点
--104-->这个request从属的request_queue
--117-->组成这个request的bio链表的头指针
--118-->组成这个request的bio链表的尾指针
--120-->内核hash表头指针

#bio

bio用来描述单一的I/O请求,它记录了一次I/O操作所必需的相关信息,如用于I/O操作的数据缓存位置,,I/O操作的块设备起始扇区,是读操作还是写操作等等

46 struct bio {47         struct bio              *bi_next;48         struct block_device     *bi_bdev;49         unsigned long           bi_flags;50         unsigned long           bi_rw;54         struct bvec_iter        bi_iter;59         unsigned int            bi_phys_segments;65         unsigned int            bi_seg_front_size;66         unsigned int            bi_seg_back_size;68         atomic_t                bi_remaining;70         bio_end_io_t            *bi_end_io;72         void                    *bi_private;85         unsigned short          bi_vcnt;91         unsigned short          bi_max_vecs;
104         struct bio_vec          bi_inline_vecs[0];
105 };
struct bio

--47-->指向链表中下一个bio的指针bi_next
--50-->bi_rw低位表示读写READ/WRITE, 高位表示优先级
--90-->bio对象包含bio_vec对象的数目
--91-->这个bio能承载的最大的io_vec的数目
--95-->该bio描述的第一个io_vec
--104-->表示这个bio包含的bio_vec变量的数组,即这个bio对应的某一个page中的一"段"内存

#bio_vec

描述指定page中的一块连续的区域,在bio中描述的就是一个page中的一个"段"(segment)

25 struct bio_vec {26         struct page     *bv_page;27         unsigned int    bv_len;28         unsigned int    bv_offset;29 };

struct bio_vec
--26-->描述的page
--27-->描述的长度
--28-->描述的起始地址偏移量

#bio_iter

用于记录当前bvec被处理的情况,用于遍历bio

31 struct bvec_iter {32         sector_t                bi_sector;34         unsigned int            bi_size;3536         unsigned int            bi_idx;40 };

#__rq_for_each_bio()

遍历一个request中的每一个bio

 739         if ((rq->bio))                  \740                 for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)


#bio_for_each_segment()

遍历一个bio中的每一个segment

242 #define bio_for_each_segment(bvl, bio, iter)                            \
243         __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)


#rq_for_each_segment()

遍历一个request中的每一个segment

742 #define rq_for_each_segment(bvl, _rq, _iter)                    \743         __rq_for_each_bio(_iter.bio, _rq)                       \744                 bio_for_each_segment(bvl, _iter.bio, _iter.iter)

遍历request_queue,绑定函数的一个必要的工作就是将request_queue中的数据取出, 所以遍历是必不可少的, 针对有IO调度的设备, 我们需要从中提取请求再继续操作, 对于没有IO调度的设备, 我们可以直接从request_queue中提取bio进行操作, 这两种处理函数的接口就不一样,下面的例子是对LDD3中的代码进行了修剪而来的,相应的API使用的是3.14版本,可以看出这两种模式的使用方法的不同。

sbull_init
           └── setup_device
                        ├──sbull_make_request
                        │            ├──sbull_xfer_bio
                        │            └──sbull_transfer
                        └──sbull_full_request
                                    ├──blk_fetch_request
                                    └──sbull_xfer_request
                                               ├── __rq_for_each_bio
                                               └── sbull_xfer_bio
                                                            └──sbull_transfer

static void sbull_transfer(struct sbull_dev *dev, unsigned long sector,unsigned long nsect, char *buffer, int write)
{unsigned long offset = sector*KERNEL_SECTOR_SIZE;unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE;if (write)memcpy(dev->data + offset, buffer, nbytes);elsememcpy(buffer, dev->data + offset, nbytes);
}static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
{struct bvec_iter i;struct bio_vec bvec;sector_t sector = bio->bi_iter.bi_sector;bio_for_each_segment(bvec, bio, i) {char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);sbull_transfer(dev, sector, bio_cur_bytes(bio)>>9 ,buffer, bio_data_dir(bio) == WRITE);sector += bio_cur_bytes(bio)>>9;__bio_kunmap_atomic(bio, KM_USER0);}return 0;
}static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
{struct bio *bio;int nsect = 0;__rq_for_each_bio(bio, req) {sbull_xfer_bio(dev, bio);nsect += bio->bi_size/KERNEL_SECTOR_SIZE;}return nsect;
}static void sbull_full_request(struct request_queue *q)
{struct request *req;int nsect;struct sbull_dev *dev ;int i = 0;while ((req = blk_fetch_request(q)) != NULL) {dev = req->rq_disk->private_data;nsect = sbull_xfer_request(dev, req);__blk_end_request(req, 0, (nsect<<9));printk ("i = %d\n", ++i);}
}static void sbull_make_request(struct request_queue *q, struct bio *bio)
{struct sbull_dev *dev = q->queuedata;int status;status = sbull_xfer_bio(dev, bio);bio_endio(bio, status);return;
}static struct block_device_operations sbull_ops = {.owner  = THIS_MODULE,.open   = sbull_open,.release= sbull_release,.getgeo  = sbull_getgeo,
};static void setup_device(struct sbull_dev *dev, int which)
{memset (dev, 0, sizeof (struct sbull_dev));dev->size = nsectors * hardsect_size;dev->data = vmalloc(dev->size);switch (request_mode) {case RM_NOQUEUE:dev->queue = blk_alloc_queue(GFP_KERNEL);blk_queue_make_request(dev->queue, sbull_make_request);break;case RM_FULL:dev->queue = blk_init_queue(sbull_full_request, &dev->lock);break;}dev->queue->queuedata = dev;dev->gd = alloc_disk(SBULL_MINORS);dev->gd->major = sbull_major;dev->gd->first_minor = which*SBULL_MINORS;dev->gd->fops = &sbull_ops;dev->gd->queue = dev->queue;dev->gd->private_data = dev;snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));add_disk(dev->gd);return;
}static int __init sbull_init(void)
{int i;sbull_major = register_blkdev(sbull_major, "sbull");Devices = (struct sbull_dev *)kmalloc(ndevices*sizeof (struct sbull_dev), GFP_KERNEL);for (i = 0; i < ndevices; i++)setup_device(Devices + i, i);return 0;
}

  回复「 篮球的大肚子」进入技术群聊

回复「1024」获取1000G学习资料

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

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

相关文章

机器学习算法4

文章目录精确率和召回率分类器性能评估混淆矩阵分类模型sklearn.metrics.classification_report决策树认识决策树优缺点信息的度量和作用信息熵决策树划分依据之信息增益api实战method决策树优缺点分析实例随机森林集成方法&#xff08;分类&#xff09;之随机森林学习算法skle…

[芦半山]Android native分析工具ASAN和HWASAN原理解析

ASAN和HWASAN原理解析由于虚拟机的存在&#xff0c;Android应用开发者们通常不用考虑内存访问相关的错误。而一旦我们深入到Native世界中&#xff0c;原本面容和善的内存便开始凶恶起来。这时&#xff0c;由于程序员写法不规范、逻辑疏漏而导致的内存错误会统统跳到我们面前&am…

线性回归,岭回归

文章目录线性回归回归算法回归算法之线性回归优缺点损失函数梯度下降算法LinearRegression属性加入交叉验证线性回归案例分析波士顿房价预测5.性能评测案例欠拟合与过拟合解决过拟合的方法回归算法之岭回归sklearn.linear_model.Ridge方法属性案例分析线性回归 回归算法 回归…

C 语言中,x += 5 == 4 是什么意思?

#讨论这个有意义吗&#xff1f;这个是在知乎上看到的一个问题&#xff0c;评论挺多的。其中有人提到&#xff0c;研究这个东西有什么用&#xff1f;编程的时候我们不能这么写的。我记得在大学的时候&#xff0c;我们的副院长给我们上课&#xff0c;就给我们提到&#xff0c;要习…

一次深刻的面试经历

没有吐槽&#xff0c;没有埋怨&#xff0c;没有鸡汤&#xff0c;纯分享。近期我到某名牌房地产公司参加了一次面试&#xff0c;面试岗位是企划主管&#xff0c;我把面试经历跟大家简单分享一下。面试背景&#xff1a;我一直从事广告传媒工作&#xff0c;在工作中服务过不同的客…

Linux内核设计的艺术

Linux内核设计的艺术这本书是我认为对Linux内核描述非常优秀的书籍。书籍中描述了内核启动的流程&#xff0c;内核运行的机理&#xff0c;内存管理&#xff0c;进程管理等等。#书籍目录第1章 从开机加电到执行main函数之前的过程11.1 启动BIOS&#xff0c;准备实模式下的中断向…

sklearn

文章目录机器学习机器怎样学习机器学习的两种方式用处监督学习三要素监督学习概念与数学形式统计学习三要素模型策略两大策略监督学习三大问题分类问题精确率与召回率标注问题回归问题无监督学习无监督学习主要方法无监督学习之聚类分析用途高斯混合模型密度分布估计协方差估计…

Tomcat 在mac上(Idea)端口冲突解决办法

Port already in use: 1099 在mac上解决办法直接是找到占用1099端口的pid&#xff1b; 解决方式&#xff1a; lsof -i:1099 回车&#xff0c;之后会有pid&#xff0c;然后执行 kill (pid号) 最后问题就得到了解决&#xff01; 注意&#xff1a;有的时候kill不能将占用端口的pid…

调试LCD反被调戏了

相关文章调试&#xff0c;是一件有挑战的事情这篇文章的题目应该写做 - 我又被LCD艹了一个晚上。写个文档简单总结下#LCD显示的一些基本概念数字视频的基本概念源自于模拟视频。对于模拟视频我们可以这样理解&#xff1a;视频可以分解为若干个基本视点&#xff08;像素&#xf…

被LCD调戏睡不着了

好吧&#xff0c;我承认我不是因为被调戏睡不着的&#xff0c;我是因为今天晚上喝了一杯该死的咖啡&#xff0c;然后就睡不着了&#xff0c;这个点[3&#xff1a;40]在床上翻来覆去&#xff0c;刚开始我摸着楠哥的小腿&#xff0c;过了一会&#xff0c;觉得没意思了&#xff0c…

线性表、顺序表

文章目录线性表、顺序表线性表概念线性结构特点线性表概念两种分类顺序表代码遍历查找插入删除逆置链表&#xff08;用指针实现变长的先行存储结构&#xff09;特点习题线性表、顺序表 线性表概念 线性结构特点 存在唯一一个被称为“第一个”的数据元素存在唯一一个被称为“最…

Linux 5.7 将支持国产 RISC-V 芯片 K210

这是转载的一篇文章&#xff0c;文章主要内容是Linux合入了一个国产芯片k210的代码&#xff0c;虽然这个芯片不是很强大&#xff0c;但是对于学习来说非常有意义&#xff0c;而且&#xff0c;还有人在这个开发板上移植了Linux 0.11。今天早上我在查阅 Linux 内核邮件列表的时候…

动态路由选择协议(二)距离矢量路由选择协议

大多数的路由选择协议属于下面二者之一&#xff1a; 距离矢量&#xff08;distance vector&#xff09;和链路状态&#xff08;link state&#xff09;。 本篇学习的是距离矢量路由选择协议的基础。 大多数的距离矢量算法是R.E.Bellman、L.R.Ford和D.R.Fulkerson所做的工作为基…

我和蓝牙BT,BLE有一腿

你好&#xff0c;很不幸&#xff0c;你被一个标题骗了进来&#xff0c;可能我以后还会骗你&#xff0c;我这篇文章主要是写蓝牙相关的&#xff0c;不会涉及技术细节&#xff0c;主要是总结一些概念&#xff0c;这些概念会帮助你了解蓝牙知识&#xff0c;帮助你在面试或者聊天的…

循环链表、双链表

文章目录循环链表、双链表双链表插入删除单循环链表双循环链表习题线性表实现方法比较循环链表、双链表 双链表 插入 删除 单循环链表 双循环链表 习题 线性表实现方法比较

[芦半山]Binder的异常机制

文中代码分析基于Android 10.0 (Q)两个进程之间若是要进行Binder通信&#xff0c;那么发起通信的一端我们就称它为Client进程。Client进程调用每一个代理对象的方法&#xff0c;本质上都是一次跨进程通信。如果这个方法是同步方法&#xff08;非oneway修饰&#xff09;&#xf…

消息中间件核心实体(1)

接上一篇《消息中间件核心实体(0)》&#xff0c;这一篇继续介绍消息中间件中的一些实体。 上一篇主要是Message、Topic、TopicMeta和Queue这样最基础的实体&#xff0c;这几篇介绍一些发送和消费的过程中会涉及到的实体和组件。 1. 发送 1.1 增强Message属性 Message一般只包含…

sklearn(2

算法库顶层设计 SKLEARN监督学习模块 近邻算法 neighbors支持向量机SVM岭回归 kernal_ridge判别分析discriminant_analysis广义线性模型linear_model集成方法ensemble决策树tree朴素贝叶斯naive_bayes交叉分解cross_decompostition高斯过程gaussian_process多层神经网络neural…

该死的装修

周末了&#xff0c;没有写技术文~因为最近的新房子需要装修&#xff0c;谈了几个装修公司&#xff0c;不知道为什么&#xff0c;我对装修这个事情非常抵触&#xff0c;繁琐的事情太多了&#xff0c;还没有装修&#xff0c;我就觉得有很多事情要斗争。因为家里有小孩&#xff0c…

SKLEARN模型选择

数据集划分方法 K折交叉验证法 将全部训练集S分成k个不相交的子集&#xff0c;假设S中的训练样例子数为m&#xff0c;那么每一个子集有m/k个训练样例&#xff0c;相应子集称作{s1,s2……sk}每次从分好的子集里面&#xff0c;拿一个作为测试集&#xff0c;其他k-1作为训练集在k…