Linux块设备驱动

  • 块设备驱动
    • 块设备驱动的引入
      • 1. 简单字符驱动程序思想
      • 2. 块设备驱动程序思想
    • 块设备驱动框架
      • 1. 层次框架
      • 2. 分析ll_rw_block
    • 块设备驱动程序编写
      • 1.分配 gendisk 结构体,使用allock_disk函数
      • 2.设置gendisk结构体
      • 3.注册gendisk结构体,使用add_disk函数
  • MTD块设备驱动
    • MTD块设备驱动框架
      • MTD字符设备读写
      • MTD块设备读写
    • MTD_FLASH驱动的编写
      • SPI_NOR驱动程序编写
      • NOR驱动编写
      • NAND驱动编写

块设备驱动

块设备驱动的引入

1. 简单字符驱动程序思想


​当应用程序的 open,read,write 等函数要操作“硬件”时,自然引入了“驱动程序”的概念,最简单的方式是 APP 调用 open 时,驱动程序的“drv_open”函数被调用等等。

2. 块设备驱动程序思想


​若块设备驱动程序也按照以上字符驱动程序的简单思想来写:

  • 硬盘

​磁盘的读写其实非常快,慢在机械结构读写装置的定位上面,从一个“磁头”的某“柱面”某“扇区”读到数据后(步骤 R0),跳到另一个“磁头”的某“柱面”的某“扇区”去写(步骤 W),接着再跳回原“磁头”相同柱面的下一个“扇区”去读(步骤R1)。慢就慢在读写扇区的跳转过程中。若按“字符设备”中的“opne” ,“read”,"write"方式,则总体效率在硬盘的读写上会非常低。
​上面过程是R0 ->W->R1,这个步骤跳转 2 次。如何优化这个步骤,增加读写效率,步骤为: R0->R1->W。这个步骤跳转 1 次,这样效率就会高些。
​总结:先不执行而是放入队列,优化后再执行(对硬盘有这种要求)。用“字符设备驱动”程序那样读写时就会在硬盘上跳来跳去,整体效率会非常低。所以有必要引入“优化过程”,就是读写先不执行,先放到某个“队列”中去。(调整顺序)

  • Flash


对于Flash,是“块”里有一个一个的扇区,Flash一般是要先擦除再写,并且擦除是以块为单位的。假如现在要写“扇区0”和“扇区1”,若用字符设备驱动的读写方式来读写:
写扇区0的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 0 的数据
③ 这时再擦除整块
④ 再把修改过扇区 0 的数据的 buf 烧写到整块
写扇区1的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 1的数据
③ 这时再擦除整块
④ 再把修改过扇区 1的数据的 buf 烧写到整块
那么要修改多个扇区时,会擦除烧写多次,总体效率会很低

优化:
① 先不执行
② 合并操作后再执行
合并:合并后只需要一次。
a. 读出整块到 buf 中
b. 在 buf 中修改扇区 0 和扇区 1
c. 擦除
d. 烧写

综上,不论是Flash还是硬盘,块设备不能像字符设备那样提供读写函数,这样效率比较低。其读写优化的思想为:① 先把读写放入队列,先不执行。② 优化后再执行。

块设备驱动框架

1. 层次框架


对于一个普通文件 1.txt 的读写会转成对块设备的读写,即要读写哪个扇区。从文件的读写转成对扇区的读写,中间会涉及到“文件系统”。应用程序读写一个普通的文件,最终会转换成操作硬件,由“块设备驱动程序”来完成。
​普通的文件如何转换成对扇区的读写,是由“文件系统”转换完成的,最终会调用到ll_rw_block这个通用的函数,ll_rw_block会把“读写”放入队列,调用队列的处理函数去优化(调整顺序、合并)再执行。(如何知道是ll_rw_block,可以看《LINUX内核源代码情景分析》)文件系统不是我们关心的重点,分析块设备驱动程序,从ll_rw_block这个函数开始分析。

2. 分析ll_rw_block

​文件系统把一个普通文件的读写转换成块设备扇区的读写,最终会调用这个底层的函数ll_rw_block,作用为:

  • 把“读写”放入队列
  • 调用队列的处理函数

这个函数在内核“fs”目录下,该目录下有各种各样的文件系统以及很多通用的文件,该函数在buffer.c中,buffer.c是fs目录下的通用文件。

/** 参数1 - op:表示是读或是写* 参数2 - op_flags:op的标志位* 参数3 - nr:bhs数组的个数* 参数4 - bhs[]:buffer_head类型的数组,表示数据传输的三要素(源,目的和长度)*/
void ll_rw_block(int op, int op_flags,  int nr, struct buffer_head *bhs[]);/* struct buffer_head 结构代表一个要进行读或者写的数据块 */
struct buffer_head {...sector_t b_blocknr;      /* 数据块号 */size_t b_size;            /* 数据块大小 */char *b_data;            /* 数据块内容 */struct block_device *b_bdev; /* 数据块所属设备 */...
};
/* ll_rw_block流程梳理 */
for (i = 0; i < nr; i++) //开始就是for循环struct buffer_head *bh = bhs[i]; //bh 等于这个参数4数组的某一项submit_bh(op, op_flags, bh); //提交buffer_head结构的bhstruct bio *bio; //使用buffer_head来构造bio(block input/output)submit_bio(bio); //提交biogeneric_make_request(bio); //使用bio构造请求,把请求放入队列struct request_queue *q = bdev_get_queue(bio->bi_bdev); //找到队列ret = q->make_request_fn(q, bio);  //调用队列中的make_request_fn函数/* q->make_request_fn函数是什么 */
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)blk_init_queue_node(rfn, lock, NUMA_NO_NODE);q = blk_init_allocated_queue(uninit_q, rfn, lock);q->request_fn = rfn;  //队列的处理函数为rfnblk_queue_make_request(q, blk_queue_bio);q->make_request_fn = blk_queue_bio; //make_request_fn的默认函数为“blk_queue_bio”/* blk_queue_bio函数流程梳理 */
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)/* 注:elv是电梯调度算法。以elv 算法把bio合并到请求队列 *//* 电梯调度算法:假如1楼A要上,2楼B要下,三楼C要上,则电梯不会让A上了再让B下了最后再跑到3楼让C上,而是让电梯从1楼上到头,再下来,一次传送同样方向的人,在一个方向上合并起来运输 */el_ret = elv_merge(q, &req, bio); //merge合并。这里先尝试合并。init_request_from_bio(req, bio); //若合并不成功,就用bio构造请求add_acct_request(q, req, where); //把请求放到队列中去__blk_run_queue(q); //执行队列__blk_run_queue_uncond(q);q->request_fn(q); //其实执行队列的方式就是调用队列的处理函数(request_fn)

块设备驱动程序编写

参考:drivers\block\z2ram.c,使用内存模拟硬盘
LINUX 驱动程序中各种驱动都构造了一个结构体 ,块设备驱动也有一个结构体gendisk ,表示磁盘设备。

1.分配 gendisk 结构体,使用allock_disk函数

static struct gendisk *z2ram_gendisk;
z2ram_gendisk = alloc_disk(1);

​alloc_disk(int minors)需要参数“minors”是指次设备号个数,即“分区个数+0”,0是指整个磁盘。当minors=1时,就是把整个磁盘当成一个分区,则不能再创建其他分区,如写成16,则最多可以创建15个分区。对于一个块设备,次设备为“0”时,表示整个磁盘。如“/dev/sda”。次设备号“1”、“2”等表示磁盘的第几个主分区,次设备号从“5”开始是“扩展分区”

2.设置gendisk结构体

​① 分配/设置一个队列:request_queue,提供读写能力,使用blk_init_queue函数

static struct request_queue *z2_queue;
static void do_z2_request(struct request_queue *q)
{struct request *req;req = blk_fetch_request(q);while (req) {...if (rq_data_dir(req) == READ)memcpy(buffer, (char *)addr, size);elsememcpy((char *)addr, buffer, size);...}
}
/** 参数1,执行处理队列的函数* 参数2,一个自旋锁 static DEFINE_SPINLOCK(z2ram_lock);*/
z2_queue = blk_init_queue(do_z2_request, &z2ram_lock);/* 之前分析把“文件读写”转成“扇区读写”最终调用ll_rw_block,将“扇区的读写”会放入这个队列里面:* 把“buffer_head”构造为“bio”,把“bio”放入队列,调用队列的“q->make_request_fn”,即blk_queue_bio函数* 当我们初始化队列时,提供了一个默认的构造请求的函数blk_queue_bio。这个函数最终会执行到q->request_fn(q),即do_z2_request函数来处理读写*/

​② 设置gendisk其他信息。(提供磁盘属性:磁盘容量,扇区大小等)

/* 主设备号 */
z2ram_gendisk->major = Z2RAM_MAJOR;
/* 第一个次设备号是什么和块设备的名字 */
z2ram_gendisk->first_minor = 0;
sprintf(z2ram_gendisk->disk_name, "z2ram");
/* fops:操作函数,可以是空函数 */
z2ram_gendisk->fops = &z2_fops;
/* 设置队列,将z2_queue放到z2ram_gendisk里 */
z2ram_gendisk->queue = z2_queue;
/* 容量:设置容量时,时以扇区为单位 */
set_capacity(z2ram_gendisk, z2ram_size >> 9);

3.注册gendisk结构体,使用add_disk函数

/* 注册gendisk结构体 */
add_disk(z2ram_gendisk);
/* 注册块设备,为块设备分配Z2RAM_MAJOR主设备号,使其在cat /proc/device能看到该块设备 */
blk_register_region(MKDEV(Z2RAM_MAJOR, 0), Z2MINOR_COUNT, THIS_MODULE, z2_find, NULL, NULL);

MTD块设备驱动

MTD块设备驱动框架


块设备驱动可以分为ramblock、硬盘、emmc和MTD等

  • 文件系统:知道怎么去优化读写 - ll_rw_block
  • 块设备驱动:知道怎么去优化读写 - gendisk结构体
  • MTD字符设备驱动:/dev/mtdchar.c
  • MTD块设备驱动:/dev/mtdblock.c和/dev/mtd_blkdevs.c
  • 硬件协议:知道发送什么去擦除、读写(发送什么指令)
  • 硬件操作:知道怎么发送命令和地址(NAND控制器、spi通讯等)

​对于不同的Flash,例如NOR、NAND和SPI_NOR,其硬件协议和硬件操作不同,但使用同一个MTD驱动框架,最终都是构造一个mtd_info结构体,然后调用mtd_device_parse_register函数去解析注册MTD设备,生成对应的字符设备和块设备。

struct mtd_info {u_char type;        /* MTD类型,包括MTD_NORFLASH,MTD_NANDFLASH等(可参考mtd-abi.h) */uint32_t flags;       /* MTD属性标志,MTD_WRITEABLE,MTD_NO_ERASE等(可参考mtd-abi.h) */uint64_t size;        /* MTD设备的大小 */uint32_t erasesize;   /* MTD设备的擦除单元大小 */uint32_t writesize;   /* 最小的可写单元的字节数,对nor是字节,对nand为一页 */uint32_t writebufsize; /* MTD写缓冲区大小 */uint32_t oobsize;   /* OOB字节数 */uint32_t oobavail;  /* 可用的OOB字节数 */...const char *name;   /* 名字 */int index;          /* 索引号 */...int (*_erase) (struct mtd_info *mtd, struct erase_info *instr); /* 擦除函数 */int (*_read) (struct mtd_info *mtd, loff_t from, size_t len,size_t *retlen, u_char *buf);     /* 读函数 */int (*_write) (struct mtd_info *mtd, loff_t to, size_t len,size_t *retlen, const u_char *buf);  /* 写函数 */...void *priv;struct module *owner;struct device dev;
};

分析mtd_device_parse_register

/** 参数1,mtd-mtd_info原始设备结构体* 参数2,types-分区解析类型,可以是cmdlinepart和ofpart* 参数3,parser_data-代解析的分区数据,由cmdline或者设备树传入* 参数4,parts-分区信息(写死在代码中,不由cmdline或设备树决定)* 参数5,nr_parts-分区个数*/
int mtd_device_parse_register(struct mtd_info *mtd, const char * const *types,struct mtd_part_parser_data *parser_data,const struct mtd_partition *parts,int nr_parts)ret = parse_mtd_partitions(mtd, types, &parsed, parser_data); //解析分区ret = mtd_add_device_partitions(mtd, &parsed); // 添加mtd设备(分区)ret = add_mtd_device(mtd); // 添加mtd设备struct mtd_notifier *not;/* drivers/mtd/mtdchar.c中已注册了主设备号为90的mtd字符设备 *//* #define MTD_DEVT(index) MKDEV(MTD_CHAR_MAJOR, (index)*2) */mtd->dev.devt = MTD_DEVT(i); error = device_register(&mtd->dev); // 创建dev/mtd*设备device_create(&mtd_class, mtd->dev.parent, MTD_DEVT(i) + 1, NULL,"mtd%dro", i); //创建dev/mtd*ro设备list_for_each_entry(not, &mtd_notifiers, list)not->add(mtd);  //遍历链表mtd_notifiers,调用not的add函数/* mtd_notifiers在哪设置? 看drivers/mtd/mtdblock.c和drivers/mtd/mtd_blkdevs.c*/
/* mtdblock.c的入口函数 */
static int __init init_mtdblock(void)register_mtd_blktrans(&mtdblock_tr);list_add(&tr->list, &blktrans_majors);  //tr即mtdblock_trregister_mtd_user(&blktrans_notifier);list_add(&new->list, &mtd_notifiers); //new即blktrans_notifier/* not->add即blktrans_notifier的add函数blktrans_notify_add */
/* mtd_blkdevs.c中 */
static struct mtd_notifier blktrans_notifier = {.add = blktrans_notify_add,.remove = blktrans_notify_remove,
};/* 分析blktrans_notify_add */
static void blktrans_notify_add(struct mtd_info *mtd)struct mtd_blktrans_ops *tr;list_for_each_entry(tr, &blktrans_majors, list)tr->add_mtd(tr, mtd);  //遍历链表blktrans_majors,调用tr的add_mtd函数
/* blktrans_majors在哪设置?同样看mtdblock.c的入口函数 */
/* tr->add_mtd即mtdblock_tr的add_mtd函数mtdblock_add_mtd */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {.name        = "mtdblock",.major      = MTD_BLOCK_MAJOR,.part_bits   = 0,.blksize   = 512,.open        = mtdblock_open,.flush     = mtdblock_flush,.release  = mtdblock_release,.readsect   = mtdblock_readsect,.writesect = mtdblock_writesect,.add_mtd  = mtdblock_add_mtd,.remove_dev = mtdblock_remove_dev,.owner       = THIS_MODULE,
};/* 分析mtdblock_add_mtd */
static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)add_mtd_blktrans_dev(&dev->mbd)gd = alloc_disk(1 << tr->part_bits);new->rq = blk_init_queue(mtd_blktrans_request, &new->queue_lock);//mtd_blktrans_request就是块设备最终的读写处理函数gd->queue = new->rq;device_add_disk(&new->mtd->dev, gd); /* 也就是add_disk */
/* add_disk实际也是调用device_add_disk */
static inline void add_disk(struct gendisk *disk)
{device_add_disk(NULL, disk);
}

MTD字符设备读写

drivers/mtd/mtdchar.c

static const struct file_operations mtd_fops = {.owner      = THIS_MODULE,.read        = mtdchar_read,.write      = mtdchar_write,.unlocked_ioctl    = mtdchar_unlocked_ioctl,.open     = mtdchar_open,.release    = mtdchar_close,...
};static ssize_t mtdchar_read(struct file *file, char __user *buf, size_t count,loff_t *ppos)ret = mtd_read(mtd, *ppos, len, &retlen, kbuf);ret_code = mtd->_read(mtd, from, len, retlen, buf); //最终调用mtd_info结构体中的_read函数static ssize_t mtdchar_write(struct file *file, const char __user *buf, size_t count,loff_t *ppos)ret = mtd_write(mtd, *ppos, len, &retlen, kbuf);mtd->_write(mtd, to, len, retlen, buf);//最终调用mtd_info结构体中的_write函数static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg)case MEMERASE64:ret = mtd_erase(mtd, erase);mtd->_erase(mtd, instr); //最终调用mtd_info结构体中的_erase函数

MTD块设备读写

drivers/mtd/mtd_blkdevs.c
最终调用mtd_blktrans_request处理函数

static void mtd_blktrans_request(struct request_queue *rq)queue_work(dev->wq, &dev->work); //将dev->work添加到dev->wq工作队列中,并唤醒相应的线程处理函数/* 这个工作队列是在哪里设置,工作又是在哪里初始化的? */
add_mtd_blktrans_dev(&dev->mbd)new->wq = alloc_workqueue("%s%d", 0, 0, tr->name, new->mtd->index);INIT_WORK(&new->work, mtd_blktrans_work);/* mtd_blktrans_work处理函数 */
static void mtd_blktrans_work(struct work_struct *work)do_blktrans_request(dev->tr, dev, req);if (rq_data_dir(req) == READ)tr->readsect(dev, block, buf)elsetr->writesect(dev, block, buf)
/* tr在哪里定义 */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {.name        = "mtdblock",.major      = MTD_BLOCK_MAJOR,.part_bits   = 0,.blksize   = 512,.open        = mtdblock_open,.flush     = mtdblock_flush,.release  = mtdblock_release,.readsect   = mtdblock_readsect,.writesect = mtdblock_writesect,.add_mtd  = mtdblock_add_mtd,.remove_dev = mtdblock_remove_dev,.owner       = THIS_MODULE,
};
tr->readsect(dev, block, buf)mtdblock_readsectdo_cached_readret = mtd_read(mtd, pos, size, &retlen, buf);mtd->_read //最终调用mtd_info结构体中的_read函数tr->writesect(dev, block, buf)do_cached_writeerase_writeret = mtd_erase(mtd, &erase);mtd->_erase    //最终调用mtd_info结构体中的_erase函数ret = mtd_write(mtd, pos, len, &retlen, buf);mtd->_write //最终调用mtd_info结构体中的_write函数

MTD_FLASH驱动的编写

关键:设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备

SPI_NOR驱动程序编写

参考drivers\mtd\devices\m25p80.c

① 分配spi_nor结构体

② 设置spi_nor结构体

...
nor->read = m25p80_read;
nor->write = m25p80_write;
nor->write_reg = m25p80_write_reg;
nor->read_reg = m25p80_read_reg;
...

③ 硬件相关设置,在m25p80_write_reg等函数中使用spi的通讯方式

④ 使用spi_nor_scan函数,在里面设置mtd_info结构体,同时也涉及到spi_nor的硬件协议,发送什么指令进行什么操作,设置了读、写、擦、使能spi_nor等指令

int spi_nor_scan(struct spi_nor *nor, const char *name, enum read_mode mode)
{struct mtd_info *mtd = &nor->mtd;...info = spi_nor_read_id(nor); //读flash的id,从内核维护的spi_nor_ids表格中获取具体型号的flash信息.../* 设置mtd_info结构体 */mtd->type = MTD_NORFLASH; mtd->writesize = 1;mtd->flags = MTD_CAP_NORFLASH;mtd->size = info->sector_size * info->n_sectors;mtd->_erase = spi_nor_erase;mtd->_read = spi_nor_read;.../* 设置操作指令 */nor->erase_opcode = SPINOR_OP_SE;nor->read_opcode = SPINOR_OP_READ;nor->program_opcode = SPINOR_OP_PP;...
}

⑤ 使用mtd_device_parse_register注册mtd设备

NOR驱动编写

参考drivers\mtd\maps\physmap.c

① 分配map_info结构体

② 设置物理基地址(phys), 大小(size), 位宽(bankwidth), 虚拟基地址(virt),并初始化

info->map[i].name = dev_name(&dev->dev);
info->map[i].phys = dev->resource[i].start;
info->map[i].size = resource_size(&dev->resource[i]);
info->map[i].bankwidth = physmap_data->width;
info->map[i].virt = devm_ioremap(&dev->dev, info->map[i].phys,info->map[i].size);
simple_map_init(&info->map[i]);

③ 使用do_map_probe函数,返回mtd_info结构体

info->mtd[i] = do_map_probe(*probe_type, &info->map[i]);
//*probe_type表示使用不同的标准
static const char * const rom_probe_types[] = {"cfi_probe", "jedec_probe", "qinfo_probe", "map_rom", NULL };do_map_proberet = drv->probe(map);cfi_probemtd_do_chip_probe(map, &cfi_chip_probe); //使用cfi标准从flash中读出相关信息jedec_probemtd_do_chip_probe(map, &jedec_chip_probe);  //从内核维护的jedec_table匹配对应型号的nor

④ 使用mtd_device_parse_register注册mtd设备

NAND驱动编写

参考drivers\mtd\maps\nand\hifmc100\hifmc100_os.c

① 分配nand_chip结构体

② 设置nand_chip结构体

③ 使用nand_scan函数设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备

static int hisi_spi_nand_probe(struct platform_device *pltdev)struct hifmc_host *host;struct nand_chip *chip;struct mtd_info *mtd;/* 分配nand_chip结构体 */host = devm_kzalloc(dev, len, GFP_KERNEL);host->chip = chip = (struct nand_chip *)&host[1];host->mtd  = mtd  = nand_to_mtd(chip);/* 设置nand_chip结构体 */result = hifmc100_spi_nand_init(chip);/* 使用nand_scan函数设置mtd_info结构体 */result = hifmc_nand_scan(mtd);nand_scan(mtd, chip_num)/* 使用mtd_device_parse_register注册mtd设备 */result = mtd_device_register(mtd, NULL, 0);mtd_device_parse_register(mtd, NULL, NULL, NULL, 0)

Linux块设备驱动-MTD子系统相关推荐

  1. Linux块设备驱动总结

    <Linux设备驱动程序>第十六章 块设备驱动程序读书笔记 简介 一个块设备驱动程序主要通过传输固定大小的随机数据来访问设备 Linux内核视块设备为与字符设备相异的基本设备类型 Linu ...

  2. linux块设备驱动(一)——块设备概念介绍

    linux块设备驱动(一)--块设备概念介绍 本文来源于: 1. http://blog.csdn.net/jianchi88/article/details/7212370 2. http://bl ...

  3. STM32MP157驱动开发——Linux块设备驱动

    STM32MP157驱动开发--Linux块设备驱动 一.简介 二.驱动开发 1.使用请求队列的方式 2.测试① 3.不使用请求队列的方式 4.测试② 参考文章:[正点原子]I.MX6U嵌入式Linu ...

  4. linux 块设备驱动(二)——块设备数据结构

    linux 块设备驱动(二)--块设备数据结构 本文来源于: 1. http://www.cnblogs.com/dyllove98/archive/2013/07/01/3165567.html 块 ...

  5. linux 块设备驱动 (三)块设备驱动开发

    linux 块设备驱动 (三)块设备驱动开发 一: 块设备驱动注册与注销 块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是 register_blkdev(),其原型为: int ...

  6. [转]linux 块设备驱动

    转自 linux块设备IO栈 http://www.sysnote.org/2015/08/06/linux-io-stack/ linux块设备IO流程 驱动 https://www.cnblogs ...

  7. Linux块设备驱动(二)————块设备的体系架构

    块设备的体系架构从上到下依次为VFS虚拟文件系统.磁盘缓冲.各种类型的磁盘系统.通用块设备层.I/O调度层(优化访问上层的请求(读写请求)).块设备驱动层.块设备硬件层. 1.虚拟文件系统(VFS) ...

  8. LINUX块设备驱动6

    第6章 +---------------------------------------------------+ |                 写一个块设备驱动                 ...

  9. linux块设备驱动编写,Linux内核学习笔记 -49 工程实践-编写块设备驱动的基础

    块设备可以随机存储.字符设备,比如键盘,只能按照输入顺序存取,不可随机,打乱输入的字节流. 文件系统层,包括常见的文件系统,以及虚拟文件系统层VFS,字符设备可以直接用应用程序打开.块设备不会在应用程 ...

最新文章

  1. ggtree实现系统发育树可视化
  2. 开发日记-20190608 关键词 读书笔记《鸟哥的Linux私房菜-基础学习篇》
  3. WebLogic Server的单点登陆功能--转载
  4. win10win键无反应_最新Science:强烷基CH键的无定向硼化作用
  5. CodeForces - 1215C Swap Letters(暴力+思维+模拟)
  6. mysql查询条件为or_使用mysql查询where条件里的or和and
  7. Android IOS WebRTC 音视频开发总结(二六)-- webrtc调用堆栈
  8. c# DESEncrypt 加密、解密算法
  9. php正则表达式中的字符是,PHP_PHP正则表达式中的特殊字符,字符/意义:对于字符,通常表 - phpStudy...
  10. 关于Hive在主节点上与不在主节点上搭建的区别之谈
  11. javaScript常见的五种数组去重(转载)
  12. Linux设备驱动开发入门之——hello驱动
  13. Spring Boot 集成 Prometheus
  14. Python数据分析与应用_从数据获取到可视化题库及答案
  15. IBM x3690 x5服务器安装Debian Linux
  16. OLED TFT屏幕相关
  17. 人工智能在产业化进程中,应同时关注基础科学的研究
  18. SQL Server附加数据库错误5123,另一个进程正在调用
  19. 长沙北大青鸟java 学费_长沙北大青鸟学校好不好 长沙北大青鸟实力学费一览表:Java代码编写规范(二)...
  20. 美国雷曼兄弟公司简介

热门文章

  1. Linux桌面系统简介
  2. 数据结构之线性表的含义和线性表的抽象数据类型
  3. vscode 安装TortoiseSVN
  4. 谷歌图标异常空白修复
  5. 沈阳工程学院计算机专业好吗,沈阳工程学院什么专业好
  6. java取上一个月_java获取当前上一周、上一月、上一年的时间
  7. C++ MOOC 西安交通大学 中国大学生MOOC网 期末考试
  8. BI需求调研四项及调研模板目录
  9. python tkinter实现俄罗斯方块基础版 —— 五、后续优化
  10. 促销后台-客服批量发券实现方案