Linux Storage入门学习

前言

本文大量代码基于linux 0.11,因为早期linux的版本更加适合初学者入门。虽然代码比较早,但是不妨碍我们学习Linux Storage的精髓。

一、hello world

1.1 Demo

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main(int argc, char *argv[]) {printf("pid = %d\n", getpid());int fd1,fd2;char s[] = "hello world\n";//打开文件,拿到fdfd1 = open("/tmp/1.txt", O_RDWR | O_CREAT);printf("fd1 = %d\n", fd1);//写入write(fd1, s, sizeof(s));close(fd1);fd2 = open("/tmp/2.txt", O_RDWR | O_CREAT);printf("fd2 = %d\n", fd2);//打开文件,拿到fdfd1 = open("/tmp/1.txt", O_RDWR);printf("fd1 = %d\n", fd1);char buffer[80];//读取read(fd1, buffer, sizeof(buffer));//关闭fdprintf("%s", buffer);getchar();//暂停程序close(fd1);close(fd2);return 0;
}

运行结果

pid = 14378
fd1 = 3
fd2 = 3
fd1 = 4
hello world


1.1 fd只是一个数字,代表数字和文件之前的一个映射关系

查看/proc/14378/fd,可以看到映射关系

dr-x------ 2 wangbinhong tctnb  0 3月  14 16:27 .
dr-xr-xr-x 9 wangbinhong tctnb  0 3月  14 16:26 ..
lrwx------ 1 wangbinhong tctnb 64 3月  14 16:27 0 -> /dev/pts/19 //stdin
lrwx------ 1 wangbinhong tctnb 64 3月  14 16:27 1 -> /dev/pts/19 //stdout
lrwx------ 1 wangbinhong tctnb 64 3月  14 16:27 2 -> /dev/pts/19 //stderr
lrwx------ 1 wangbinhong tctnb 64 3月  14 16:27 3 -> /tmp/2.txt
lrwx------ 1 wangbinhong tctnb 64 3月  14 16:27 4 -> /tmp/1.txt

程序中的4是数字,/proc/14378/fd/4是一个文件,链接到/tmp/1.txt,/proc/14378/fd/4只存在内存中,不存在硬盘中,程序中的数字4不是指向/proc/14378/fd/4文件

二、数字fd代表什么

2.1 task_struct

每一个进程在内核中的有一个task_struct的结构体,结构体中有一个file指针数组filp,fd代表filp这个数组的下标,fd = 4 就代表filp[4]指向的file结构体

struct task_struct {...struct file * filp[NR_OPEN];...
}

2.2 file

struct file {unsigned short f_mode;//文件的类型和属性unsigned short f_flags;//文件打开的标志unsigned short f_count;//关联的fd的个数struct m_inode * f_inode;//file的真实实现off_t f_pos;//文件当前的读写指针,读到哪里了。
}

2.3 file_table

内核中还有一个全局的file_table,是file数组,保存所有file结构体。

struct file file_table[NR_FILE];//系统级的一个file table

2.4 关联关系

两个细节
dup:同进程两个fd指向同一个file
fork:两个进程的两个fd指向同一个file


下面是新的Kernel关系图


2.5 Binder传输fd

Binder传输fd,两个进程的不同fd指向了同一个file,享有相同的file offset和file status flag.

三、以pipe为例:一切皆文件

早期的错误想法

硬件驱动通过设备文件和用户空间的应用程序通信,是通过将驱动的信息写进设备文件,然后应用程序读取设备文件的内容。

3.1 pipe初始化


3.1.1 sys_pipe

系统调用pipe的实现,会返回两个fd给用户空间

int sys_pipe(unsigned long * fildes)//系统调用生成一对pipe
{struct m_inode * inode;struct file * f[2];int fd[2];int i,j;j=0;for(i=0;j<2 && i<NR_FILE;i++)if (!file_table[i].f_count)//找到空闲的file(f[j++]=i+file_table)->f_count++;//将空闲的file的f_count+1,并保存这两个file结构体if (j==1)//只找到一个f[0]->f_count=0;//将第一file重置,清空if (j<2)return -1;//没找到一队,反正就是失败j=0;for(i=0;j<2 && i<NR_OPEN;i++)//将两个file的指针分别保存到current->filp[i]的空闲处if (!current->filp[i]) {current->filp[ fd[j]=i ] = f[j];j++;}if (j==1)current->filp[fd[0]]=NULL;if (j<2) {f[0]->f_count=f[1]->f_count=0;return -1;}//和上面逻辑类似if (!(inode=get_pipe_inode())) {//详见3.1.2 获得一个pipe inodecurrent->filp[fd[0]] =current->filp[fd[1]] = NULL;f[0]->f_count = f[1]->f_count = 0;return -1;}f[0]->f_inode = f[1]->f_inode = inode;//让两个file结构体的f_inode指向pipe inodef[0]->f_pos = f[1]->f_pos = 0;//重置读写指针f[0]->f_mode = 1;       /* read */f[1]->f_mode = 2;       /* write */put_fs_long(fd[0],0+fildes);//将fd0数值返回给用户空间put_fs_long(fd[1],1+fildes);//将fd1数值返回给用户空间return 0;
}
3.1.2 get_pipe_inode

创建一个m_inode结构体
将m_inode的i_size指向一块4096B的缓冲区
设置m_inode的i_pipe为1,标识这个m_inode为pipe inode

struct m_inode * get_pipe_inode(void)//返回一个空的pipe inode用于pipe
{struct m_inode * inode;if (!(inode = get_empty_inode()))return NULL;if (!(inode->i_size=get_free_page())) {//申请一个物理页4096B作为环形管道缓冲区,缓冲区指针保存到i_size。inode->i_count = 0;return NULL;}inode->i_count = 2; /* sum of readers/writers */PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0;inode->i_pipe = 1;//表示为pipe的m_inodereturn inode;
}
3.1.3 get_free_page

申请一个物理页,并返回内核空间的地址

unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");__asm__("std ; repne ; scasb\n\t""jne 1f\n\t""movb $1,1(%%edi)\n\t""sall $12,%%ecx\n\t""addl %2,%%ecx\n\t""movl %%ecx,%%edx\n\t""movl $1024,%%ecx\n\t""leal 4092(%%edx),%%edi\n\t""rep ; stosl\n\t""movl %%edx,%%eax\n""1:":"=a" (__res):"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),"D" (mem_map+PAGING_PAGES-1):"di","cx","dx");
return __res;
}

3.2 读pipe

3.2.1 sys_read

根据inode->i_pip,将sys_read变成read_pipe。

int sys_read(unsigned int fd,char * buf,int count)//文件读的系统调用,fd->file->inode->数据块
{struct file * file;struct m_inode * inode;if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))return -EINVAL;if (!count)return 0;verify_area(buf,count);inode = file->f_inode;if (inode->i_pipe)//pipereturn (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;//3.2.2if (S_ISCHR(inode->i_mode))//字符设备return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);if (S_ISBLK(inode->i_mode))//块设备return block_read(inode->i_zone[0],&file->f_pos,buf,count);if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {//常规文件或目录if (count+file->f_pos > inode->i_size)count = inode->i_size - file->f_pos;if (count<=0)return 0;return file_read(inode,file,buf,count);}printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);return -EINVAL;
}
3.2.2 read_pipe

将缓冲区的数据读到用户空间char,读count字节

int read_pipe(struct m_inode * inode, char * buf, int count)//读取pipe
{int chars, size, read = 0;while (count>0) {while (!(size=PIPE_SIZE(*inode))) {//如果发现没有内容wake_up(&inode->i_wait);//唤醒写端if (inode->i_count != 2) /* are there any writers? *///没有写端return read;sleep_on(&inode->i_wait);//没有内容就睡眠}chars = PAGE_SIZE-PIPE_TAIL(*inode);//判断尾部的数据if (chars > count)chars = count;if (chars > size)chars = size;count -= chars;read += chars;size = PIPE_TAIL(*inode);//头部开始读的指针PIPE_TAIL(*inode) += chars;PIPE_TAIL(*inode) &= (PAGE_SIZE-1);while (chars-->0)put_fs_byte(((char *)inode->i_size)[size++],buf++);}wake_up(&inode->i_wait);return read;
}

3.3 写pipe

3.3.1 sys_write

根据inode->i_pip,将sys_write变成write_pipe。

int sys_write(unsigned int fd,char * buf,int count)//文件写的系统调用,fd->file->inode->数据块
{struct file * file;struct m_inode * inode;if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))return -EINVAL;if (!count)return 0;inode=file->f_inode;if (inode->i_pipe)return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;//3.3.2if (S_ISCHR(inode->i_mode))return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);if (S_ISBLK(inode->i_mode))return block_write(inode->i_zone[0],&file->f_pos,buf,count);if (S_ISREG(inode->i_mode))return file_write(inode,file,buf,count);printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);return -EINVAL;
}
3.3.2 write_pipe

将用户空间char对应的数据写到缓冲区,写入count字节

int write_pipe(struct m_inode * inode, char * buf, int count)//写pipe的实现
{int chars, size, written = 0;while (count>0) {while (!(size=(PAGE_SIZE-1)-PIPE_SIZE(*inode))) {//如果写满了,size为0wake_up(&inode->i_wait);//唤醒读端if (inode->i_count != 2) { /* no readers *///没有读者直接返回current->signal |= (1<<(SIGPIPE-1));return written?written:-1;}sleep_on(&inode->i_wait);//写端休眠}chars = PAGE_SIZE-PIPE_HEAD(*inode);//计算管道头部到缓冲区末端的空闲字节数 4098if (chars > count)chars = count;if (chars > size)chars = size;count -= chars;written += chars;size = PIPE_HEAD(*inode);//当前的头指正PIPE_HEAD(*inode) += chars;PIPE_HEAD(*inode) &= (PAGE_SIZE-1);while (chars-->0)((char *)inode->i_size)[size++]=get_fs_byte(buf++);//一个写字符到管道}wake_up(&inode->i_wait);//写完唤醒读端return written;
}

3.4 pipe读写指针

其实pipe的读写指针并没有保存在file结构体,而是保存在m_inode。
如果缓冲区满了,读端一会不读,会导致写端的进程sleep。
整个读写的过程,并没有通过锁来控制,而是通过ringbuffer来实现,有兴趣的可以自己研究。

struct m_inode {
...unsigned long i_size;//被作为指针指向申请的缓冲区,一个缓冲区4096Bunsigned short i_zone[9];//用i_zone[0]代表写的游标,用i_zone[1]代表写的游标
...
}


3.5 思考一个问题

父进程创建一对pipe的fd1 fd2
子进程通过fork复制父进程的pipe fd1 fd2
父进程关闭读的fd1
子进程关闭写的fd2
父子进程就可以通过pipe进行跨进程通信


3.6 Linux的改进

当文件类型越来越多的时候,用file_operations结构体代替大量if else
file_operations中保存read write的函数指针

struct file {const struct file_operations    *f_op;void            *private_data;//这个很重要
}struct file_operations {ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
};

我觉得这样子理解更加合适:一切皆文件接口

四、普通文件

4.1重要数据结构

4.1.1 m_inode

file中f_inode指向就是m_inode,也就是file对应的真实实现。
i_dev:代表块设备号
i_zone[9]:代表数据块号

struct m_inode {unsigned short i_mode;//15-12文件类型,11-9保存执行文件设置,8-0保存文件权限unsigned short i_uid;//文件宿主的用户idunsigned long i_size;//文件长度unsigned long i_mtime;//修改时间unsigned char i_gid;//文件宿主的组idunsigned char i_nlinks;//硬链接的次数unsigned short i_zone[9];//对应数据块 0-6直接是块号,7一次间接块,8二次间接块,一个块是1KB=1024Byte//因为块号用short来表示,也就是2Byte,所以一个块可以存放512个块号,所以一次块512个,二次块就是512*512。//所以变相的可以算出一个文件的最大size是7+512+512*512 kb//一般逻辑块的大小会和buffer_head大小一样。
/* these are in memory also */struct task_struct * i_wait;unsigned long i_atime;unsigned long i_ctime;unsigned short i_dev;//设备号unsigned short i_num;unsigned short i_count;unsigned char i_lock;unsigned char i_dirt;unsigned char i_pipe;unsigned char i_mount;unsigned char i_seek;unsigned char i_update;
};


4.1.2 buffer_head

b_dev设备号
b_blocknr数据块号
b_data指向一块内存
所有buffer_head会被存放在一个hash表中

struct buffer_head {char * b_data;          /* pointer to data block (1024 bytes) */unsigned long b_blocknr;    /* block number */ //块号unsigned short b_dev;       /* device (0 = free) */ //设备号unsigned char b_uptodate;unsigned char b_dirt;       /* 0-clean,1-dirty */unsigned char b_count;      /* users using this block */unsigned char b_lock;       /* 0 - ok, 1 -locked */struct task_struct * b_wait;struct buffer_head * b_prev;struct buffer_head * b_next;struct buffer_head * b_prev_free;struct buffer_head * b_next_free;
};

可以调用下面两个接口,完成数据块的读写,这背后的实现就要看块设备驱动怎么实现的。

先记住,buffer_head(设备号+块号+内存地址)+读写指令 可以完成一次信息的交换。
ll_rw_block(READ,bh);
ll_rw_block(WRITE,bh);

4.2 读文件

4.2.1 file_read

根据(filp->f_pos)/BLOCK_SIZE计算对应的块号nr
根据inode->i_dev和nr调用bread获得buffer_head
调用ll_rw_block(READ,bh)请求数据块的数据
将buffer_head中b_data拷贝到用户空间的buf

int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{int left,chars,nr;struct buffer_head * bh;if ((left=count)<=0)return 0;while (left) {if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {if (!(bh=bread(inode->i_dev,nr)))//4.2.2break;} elsebh = NULL;nr = filp->f_pos % BLOCK_SIZE;chars = MIN( BLOCK_SIZE-nr , left );filp->f_pos += chars;left -= chars;if (bh) {char * p = nr + bh->b_data;while (chars-->0)put_fs_byte(*(p++),buf++);brelse(bh);} else {while (chars-->0)put_fs_byte(0,buf++);}}inode->i_atime = CURRENT_TIME;return (count-left)?(count-left):-ERROR;
}
4.2.2 bread

根据设备号,数据块号,创建一个buffer_head

/** bread() reads a specified block and returns the buffer that contains* it. It returns NULL if the block was unreadable.*/
struct buffer_head * bread(int dev,int block)//从dev,block获得buffer_head,一般用这个就可以
{struct buffer_head * bh;if (!(bh=getblk(dev,block)))//拿一块空闲的buffer_headpanic("bread: getblk returned NULL\n");if (bh->b_uptodate)return bh;ll_rw_block(READ,bh);//将硬件的数据读取到buffer_headwait_on_buffer(bh);if (bh->b_uptodate)return bh;brelse(bh);//释放锁return NULL;
}

4.3 写文件

4.3.1 file_write

根据(filp->f_pos)/BLOCK_SIZE计算对应的块号block,如果文件不够大,需要扩容。
根据inode->i_dev和block调用bread获得buffer_head
将用户空间的buf拷贝到buffer_head中b_data。

int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{off_t pos;int block,c;struct buffer_head * bh;char * p;int i=0;/** ok, append may not work when many processes are writing at the same time* but so what. That way leads to madness anyway.*/if (filp->f_flags & O_APPEND)pos = inode->i_size;elsepos = filp->f_pos;while (i<count) {if (!(block = create_block(inode,pos/BLOCK_SIZE)))//如果写文件的时候发现文件不够大,就要扩容break;if (!(bh=bread(inode->i_dev,block)))break;c = pos % BLOCK_SIZE;p = c + bh->b_data;bh->b_dirt = 1;c = BLOCK_SIZE-c;if (c > count-i) c = count-i;pos += c;if (pos > inode->i_size) {inode->i_size = pos;inode->i_dirt = 1;}i += c;while (c-->0)*(p++) = get_fs_byte(buf++);brelse(bh);}inode->i_mtime = CURRENT_TIME;if (!(filp->f_flags & O_APPEND)) {filp->f_pos = pos;inode->i_ctime = CURRENT_TIME;}return (i?i:-1);
}
4.3.2 sys_sync

ll_rw_block将buffer_head的脏数据同步到块设备

int sys_sync(void)//系统调用,同步块设备和内存高速缓存中数据
{int i;struct buffer_head * bh;sync_inodes();      /* write out inodes into buffers *///将修改的inode数据写入到buffer_head.bh = start_buffer;for (i=0 ; i<NR_BUFFERS ; i++,bh++) {wait_on_buffer(bh);if (bh->b_dirt)ll_rw_block(WRITE,bh);//真的写到块设备中}return 0;
}

五、块设备

5.1 内部结构


struct d_inode {  //块设备中对应的inode的结构体unsigned short i_mode;unsigned short i_uid;unsigned long i_size;unsigned long i_time;unsigned char i_gid;unsigned char i_nlinks;unsigned short i_zone[9];
};
struct dir_entry { //目录unsigned short inode;//inodechar name[NAME_LEN];//inode对应的文件名
};


5.2 mount("dev/block/sda0","/sdcard")

第一步:从dev/block/sda0中读取第0块inode块上的数据,读取第一个d_inode的数据,并构建内存中的m_inode。

第二步:将m_inode的i_num和"sdcard"按照dir_entry的结构体存放在"/"目录对应m_inode指向的i_zone数据区域


5.3 int fd = open("/sdcard/1.txt")

第一步:找到m_inode("sdcard")

读取"/"对应的m_inode("/")的 i_zone[9]的数据到内存中
根据"sdcard"得到inode号,拿到"sdcard"对应的m_inode("sdcard"),m_inode已经在mount中创建。

第二步:创建m_inode("1.txt")

读取m_inode("sdcard")的 i_zone[9]的数据到内存中
根据"1.txt",拿到1.txt文件对应的d_inode的号
计算d_inode号找到对应的d_inode结构体存放在块设备的块号,块号=inode/一个块最多存放的d_inode结构体
读取的块号对应数据到内存中,读取对应的d_inode数据,构建m_inode("sdcard")

第三步:将fd指向file指向m_inode

创建file指向m_inode("1.txt")
file[fd]指向file
返回fd


从此形成fd->file->m_inode的对应关系,write read close 都可以对应的转化成file的操作,对应的m_inode的操作

六、目前Linux的架构

构建了一个VFS层,虚拟文件系统,各类文件系统可以更好的兼容,EXT4,F2FS
文件系统和块设备的数据交互,用BIO代替了buffer_head
新增了Block Layer层对BIO进行合并调度

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

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

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

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

相关文章

地铁客流检测训练问题记录

一 报错&#xff1a; File "/home/jz/py-faster-rcnn/tools/../lib/datasets/pascal_voc.py", line 183, in _load_pascal_annotation tree ET.parse(filename) File "/usr/lib/python2.7/xml/etree/ElementTree.py", line 1182, in parse tree.parse(sou…

python价值观测试程序例子_PyBrains学习迷宫的例子。国家价值观与全球政策

现在我添加了另一个约束-通过在迷宫.py现在我在1000次跑步后得到了这种行为&#xff0c;每次跑步有200次互动&#xff1a;现在哪种方式有意义-机器人试图从另一边绕墙&#xff0c;避开状态(1&#xff0c;7)所以&#xff0c;我得到了奇怪的结果&#xff0c;因为特工过去总是从随…

记一次和摄像头的摩擦经历

因为时间的原因&#xff0c;这次点亮摄像头的时间特别短&#xff0c;昨天下午模组到公司&#xff0c;今天下午点亮。几个人一起调试&#xff0c;发现的问题也很多&#xff0c;今天下午发现有一个怀疑的问题&#xff0c;我马上驱车几十公里去模组厂调试&#xff0c;回来的时候&a…

算法题

简单题 整数反转 给出一个 32 位的有符号整数&#xff0c;你需要将这个整数中每位上的数字进行反转。 示例 1: 输入: 123 输出: 321 示例 2: 输入: -123 输出: -321 示例 3: 输入: 120 输出: 21 注意: 假设我们的环境只能存储得下 32 位的有符号整数&#xff0c;则其数值范围…

ES6箭头函数(节选自《ECMAScript 6 入门》)

基本用法 ES6 允许使用“箭头”&#xff08;>&#xff09;定义函数。 var f v > v; 上面的箭头函数等同于&#xff1a; var f function(v) {return v; }; 如果箭头函数不需要参数或需要多个参数&#xff0c;就使用一个圆括号代表参数部分。 var f () > 5; // 等同于…

声明为指针,定义为数组,声明为数组,定义为指针

之前写发的那篇指针和数组的文章&#xff0c;有网友评论觉得不是很舒服&#xff0c;我自己看了&#xff0c;觉得很不满意。所以想再写写&#xff0c;尽量把这个东西讲清楚。#定义为数组&#xff0c;声明为指针1.c中#include "stdio.h" char array[] "abcd"…

python部落稿酬_Python之父考虑重构Python解释器

作者&#xff1a;佚名来源&#xff1a;Python部落7月22日&#xff0c;Python之父Guido在Medium上发表了他的第一篇博文《PEG Parser》。在该文中&#xff0c;Guido说他正在考虑使用PEG Parser代替现有的类LL(1) Parser(名为pgen)&#xff0c;来重构Python解释器。原因是现在的 …

f2fs存储结构初探

前言学习文件系统的第一步&#xff0c;先搞清楚文件系统在设备上的存储结构&#xff0c;先来简单了解一下。F2FS空间布局图和描述选自《F2FS技术拆解》https://mp.weixin.qq.com/s/k1ibtWF_TRQi8wbqUGjMrgF2FS空间布局F2FS空间布局整个存储空间被划分为6个区域&#xff1a;超级…

Android系统驱动【转】

本文转载自&#xff1a;http://www.hovercool.com/en/%E6%B7%BB%E5%8A%A0%E9%A9%B1%E5%8A%A8%E6%A8%A1%E5%9D%97#a_.E5.9B.9B.E3.80.81.E9.85.8D.E7.BD.AE.E7.B3.BB.E7.BB.9F.E7.9A.84autoconfig 一、编写驱动核心程序 这里说的驱动核心程序是指运行在内核空间的&#xff0c;完…

net.conn read 判断数据读取完毕_高并发:缓存模式以及缓存的数据一致性

缓存由于其高性能&#xff0c;支持高并发的特性&#xff0c;在高并发的项目中不可或缺。被大家广泛使用的有Redis&#xff0c;Memcached等。本文主要探讨几种常见的缓存的读写模式&#xff0c;以及如何来保证缓存和数据库的数据一致性。这里大家可以关注一下我的个人专栏《PHP进…

PHP配置环境中开启GD库

下配置好的PHP环境中&#xff0c;GD库不像windows那样可以直接用&#xff0c;而是默认关闭&#xff0c;需要把它打开&#xff0c;去到php.ini文件中 找到php_gd2.dll把分号去掉即可。(注&#xff1a;GD库跟绘制二维码等有关) 转载于:https://www.cnblogs.com/mrszhou/p/7421161…

USB协议普及文

#目录#USB#USB On-The-Go Supplement#技术指标#机械和电气标准#编码方式#软件架构#端点#HCD#USB 封包格式#设备分类#USB接头#电源#Storage#人机接口设备&#xff08;HID&#xff09;通用串行总线(Universal Serial Bus, USB&#xff09;是连接外部设备的一个串口总线标准&#…

mysql jdbc驱动_JDBC认识与实践

一、什么是JDBC&#xff1f;Java数据库连接&#xff0c;&#xff08;Java Database Connectivity&#xff0c;简称JDBC&#xff09;是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口&#xff0c;提供了诸如查询和更新数据库中数据的方法。JDBC思维导图二、JDBC应…

画布实现拼图原理

1. 页面布局 1.1 bg: 背景提示图&#xff0c;使用半透明效果&#xff0c;移动图片后显示&#xff0c;层级最低&#xff0c;z-index:-1; 1.2 cvs: 当前画布&#xff0c;层级默认0&#xff1b; 1.3 content: 覆盖在画布之上 z-index:1; 1.3.1 currentCheckpoint: 显示当前关卡数…

晚归的码农老公

呃&#xff0c;我是本号主的贤内助&#xff0c;我们的儿子叫楠哥~由于疫情的影响&#xff0c;我们已经很久没有带楠楠回他外婆家玩了&#xff0c;只能用电话跟外婆聊聊家常。每一次通话结束&#xff0c;他外婆总会加问一句&#xff0c;启发回来了吧。我们每次的回答都是&#x…

100篇文章(5)

On Libraries’ Service Library are very important to students’campus life. However,many students’complain that libraries in their universities cannot meet their needThe first problem is nearly of all school libraries is their limited space.There always …

python绘制图像的参数_图像绘制.draw.line():系统错误:新样式getargs格式,但参数不是tup...

我在这个问题上看到了多个问题&#xff0c;但没能找到问题的答案。基本上&#xff0c;我只想在图像上画一条线&#xff0c;从python中的外部文件中获取坐标。我的代码是&#xff1a;import Image, ImageDrawimport sysimport csvim Image.open("screen.png")draw I…

Rockchip USB转485

#RS232 / RS485 简介#RS232#RS485#R485与RS232比较#开发#DTS配置#驱动开发#POSIX规范API#HAL层以上APP层#APK call JNI#APP#调试#log开启#RS232/RS485/RS422常见问题#RS232 / RS485 简介#RS232RS-232是美国电子工业联盟&#xff08;EIA&#xff09;制定的串行数据通信的接口标…

100篇范文(7)

How should college students spend their spare time? College is a place where students arrange their own studies and take part in all kinds of activities as they like.Thus,plenty of time is left at college students’disposal.How should student spend their…

高嘌呤食物搜索引擎_“高嘌呤”的食物已发现,尿酸高的人,要尽量挑着吃!...

导语&#xff1a;在我们的生活中又出现了一种现象&#xff0c;越来越频发&#xff0c;也是越来越倾向于年轻化&#xff0c;那么就是高尿酸&#xff0c;现在高尿酸已经被列为了我们国家的第四高&#xff0c;身体内的嘌呤含量是一部分&#xff0c;那么另一部分也是来源于我们平时…