《ORANGE’S:一个操作系统的实现》读书笔记(二十七)文件系统(二)

上一篇文章我们记录了如何操作硬盘,并且编写了简单的硬盘驱动程序用于获取一些硬盘的参数。这篇文章就在上一篇文章的基础上记录文件系统,完善硬盘驱动程序。

文件系统

现在我们该仔细考虑如何构建一个文件系统了。这并不是我们第一次接触文件系统,我们在之前的时候就研究过FAT12。FAT12算是很简单的文件系统了,既然我们已经比较熟悉它了,就让我们结合它的结构来分析一下一个文件系统都需要哪些要素。

我们来参考一下FAT12的布局,图中分为四个部分,分别是引导区、FAT表、根目录区和数据区。其中引导扇区中不仅包含引导代码,而且包含BPB,它包含诸如根目录文件数最大值之类的信息,可算是文件系统的Metadata;FAT表记录的是整个磁盘扇区的使用情况,有哪些扇区未被使用,以及每个文件占用哪些扇区等;根目录区则是文件的索引了,那里记录了文件的名称、属性等内容。

这么看来,一个简单的文件系统大致需要这么几个要素:

  • 要有地方存放Metadata;
  • 要有地方记录扇区的使用情况;
  • 要有地方来记录任一文件的信息,比如占用了哪些扇区等;
  • 要有地方存放文件的索引。

这些要点不难理解,而且如果你分析其它文件系统的话,也基本是这些要素。与此同时,只要具备了这些要素,一个文件系统基本就可以用了——至于好坏,那不是我们这样的初学者要考虑的问题。

好了,根据这些要素,书上又同时参照了Minix的文件系统,我们就把我们的文件系统设计成如下图所示的样子。

可以看到,它几乎是把前面叙述的各要素一字排开:

  • 要有地方存放Metadata——占用整整一个扇区的super block;
  • 要有地方记录扇区的使用情况——sector map;
  • 要有地方记录任一文件的信息,比如占用了哪些扇区等——inode map以及被称作inode_array的i-node真正存放地;
  • 要有地方存放文件的索引——root数据区。

super block通常也叫做超级块,关于文件系统的Metadata我们统统记在这里。sector map是一个位图,它用来映射扇区的使用情况,用1表示扇区已被使用,0表示未被使用。i-node是UNIX世界各种文件系统的核心数据结构之一,我们把它借用过来。每个i-node对应一个文件,用于存放文件名、文件属性等内容,inode-array就是把所有i-node都放在这里,形成一个较大的数组。而inode map就是用来映射inode-array这个数组使用情况的一个位图用法跟sector map类似。root数据区类似于FAT12的根目录区,但本质上它也是个普通文件,由于它是所有文件的索引,所以我们把它单独看待。为了简单起见,我们的文件系统暂不支持文件夹,也就是说用来表示目录的特殊文件只有这么一个。这种不支持文件夹的文件系统,历史上曾经有过,而且这种文件系统还有个名字,叫做扁平文件系统(Flat File System)。

至于引导扇区,就让它纯粹用作引导吧,我们不打算学习FAT12把一些额外的数据结构塞进去——512字节已经够挤了,而如今的硬盘是如此的便宜。

轻轻松松,在前人的基础上,加上做的也简单,我们的文件系统就这样设计完成了。下面该是想想怎么将它放到硬盘上了。根据我们的经验,一个文件系统可以安装到硬盘上的一个分区上,而且一块硬盘之上可以有多个文件系统共存。那么,下面我们就来找个分区在它上面实现,可是不忙,我们还不知道硬盘是怎么分区的呢?下面就来研究一下。

硬盘分区表

你可能会有这样一个问题,就是为什么不把文件系统直接安装到整块硬盘上呢?这样做是完全可以的,而且简单易行。但是书上作者的想法是这样的,将来可以把我们辛苦实现的操作系统装到自己的计算机上,到时候稍微设置一下Grub,实现多引导,让我的操作系统跟Linux、Windows等并存,岂不美哉。所以在这里我们就多做一些,研究一下怎么来针对分区进行操作,要不然一下子用掉整块硬盘,显得过于浪费了。

硬盘分区表其实是一个结构体数组,数组的每个成员是一个16字节的结构体,它的构成如下表所示。

偏移长度描述
01状态(80h=可引导,00h=不可引导,其它=不合法)
11起始磁头号
21起始扇区号(仅用了低6位,高2位为起始柱面号的第8,9位)
31起始柱面号的低8位
41分区类型(System ID)
51结束磁头号
61结束扇区号(仅用了低6位,高2位为结束柱面号的第8,9位)
71结束柱面号的低8为
84起始扇区的LBA
124扇区数目

这个数组位于引导扇区的 1BEh 处,共有四个成员——因为IBM当时觉得一台PC最多会装四个操作系统。现在我们的计算机中每块硬盘经常划分成不止四个分区,这是因为每个主分区可以进一步分成多个逻辑分区。具体的做法,我们还是需要一个示例来对照。为了安全起见,我们操作映像而不是真的硬盘。现在我们把上一篇文章生成的硬盘分成几个区:

在这里我们把一个80MB的硬盘映像分成了一个主分区和一个扩展分区,扩展分区中又分成了五个逻辑分区(逻辑分区过程截图并未完全显示,也可以根据自己想法进行分区)。我们将来把Orange’s装在第一个逻辑分区上,也就是hd.img5的分区。我们先是把它的分区类型(System ID)改成99h,又为它设定了“可启动”标志。在设置分区类型时,我们先是列出了已知的类型,然后选定还未使用的99h作为我们文件系统的System ID。

现在我们就来实际看一下分区表是什么样子的,用二进制查看器来看一下引导扇区:

硬盘分区表位于1BEh 处,共有4个成员,每个成员是16字节,所以第1BEh到第1FDh字节便是分区表的内容了,按照硬盘分区表的说明,可知它们的意义如下表所示。

分区序号状态分区类型起始扇区LBA扇区数目
000h(不可引导)83800h5000h
100h(不可引导)055800h1E000h

从表中可知,第一个分区起始于800h扇区,共有5000h个扇区,第二个分区起始于5800h扇区,共有1E000h个扇区。然后这些信息是不够的,我们还有若干逻辑分区的信息没有得到呢。没关系,一步一步来,我们现在就来看一下第二个分区——也就是扩展分区的第一个扇区时什么样子。扩展分区的开始字节为B00000h(5800h*200h),它的内容如下:

其主要项的意义如下表所示。

分区序号状态分区类型起始扇区LBA扇区数目
080h(可引导)99h800h5000h
100h(不可引导)05h5800h5800h

前一个分区的起始扇区LBA是800h,这是个相对于扩展分区基地址的LBA,也就是说,它真正的LBA是5800h+800h=6000h。后一个分区,根据其分区类型05h可知,它又是个扩展分区,起始扇区LBA为5800h+5800h=B000h,字节偏移为B000h*200h=1600000h,我们继续看看其引导扇区:

其意义如表所示。

分区序号状态分区类型起始扇区LBA扇区数目
000h(不可引导)83800h5000h
100h(不可引导)05B000h5800h

从分区类型值(System ID)可以看出,在这个分区中,又包含了一个“普通的”分区和一个扩展分区,你现在可能有些明白了,多个逻辑分区是由嵌套来实现的。一个扩展分区里包含一个普通分区的同时,又可以嵌套一个扩展分区,一层一层的。其实这种层状结构,也可以看做是一个链表,链表的节点即为扩展分区的分区表,每个节点中有两个表项,前一个表项描述一个普通分区,后一个表项指向下一个节点。

需要留意的一点是,前一个表项中的起始扇区LBA是相对于当前扩展分区的,而后一个表项中的起始扇区——也就是下一个扩展分区的起始扇区——是相对于硬盘主引导扇区所指明的扩展分区的起始扇区的。这样说可能有点拗口,就本例来说,扇区5800h中的分区表有两个表项,前一项的起始扇区LBA为800h,它的实际LBA要将800h与5800h相加,即6000h;后一项的起始扇区LBA为5800h,它的实际地址要与5800h相加,即B000h。

明白了这些,遍历所有逻辑扇区的工作需要的就只剩下一些耐心和细心了。按照这样的方法,我们可以一步一步遍历所有的分区。

设备号

硬盘的每个分区都会有一个分区号,在我们的例子中,主引导扇区中有两个表项,对应一个主分区和一个扩展分区,即hd.img1和hd.img2,扩展分区中有5个逻辑分区,从hd.img5到hd.img9。Linux中的编号规则是1~4这四个数字为主引导扇区的分区表项所用,从5开始依次表示逻辑分区。

其实,1、2、5~9等这些数字有个名称,叫做次设备号。其作用是给每个设备(分区)起一个名字,这样驱动程序就能方便地管理它们。另外还有个我们没说过的主设备号,它的作用是给每一类设备一个名字,以方便管理。举个例子,假设我们的计算机内有三块硬盘和两个软盘。对用户而言,操作硬盘和软盘上的文件的区别可能仅在于路径不同,但对于操作系统,硬盘和软盘需要不同的驱动程序,所以不同类别的硬件需要区别对待,这就是主设备号存在的理由。同时,硬盘有多个,而且每个硬盘上可能有多个分区,对这些分区,又需要区别对待,于是又用到了次设备号。简单来说,主设备号告诉操作系统应该用哪个驱动程序来处理,次设备号告诉驱动程序这是具体哪个设备。

在我们的系统中,我们也需要有主次设备号,但对硬盘而言,我们采用与Linux不同的编号规则,具体如下图所示。

在这里我们还是只看主IDE通道上连接两块硬盘的情况。图中括号内的便是次设备号。主盘是hd0,其次设备号为0,它的主引导扇区分区表对应四个分区分别是hd1、hd2、hd3、hd4。每个扩展分区中最多有16个逻辑分区,以字母a~p表示,逻辑分区的次设备号是以hd1a为基准递增的。这种编号规则的好处是,给定一个次设备号,可以很容易地计算出它是主分区还是扩展分区,或者是哪个扩展分区的哪个逻辑分区。同时,给定一个分区的名称,我们也很容易计算出其次设备号。

配置这套规则,我们定义了一些宏。

代码 include/const.h,硬盘设备号相关的宏。

#define MAX_DRIVES          2
#define NR_PART_PER_DRIVE   4
#define NR_SUB_PER_PART     16
#define NR_SUB_PER_DRIVE    (NR_SUB_PER_PART * NR_PART_PER_DRIVE)
#define NR_PRIM_PER_DRIVE   (NR_PART_PER_DRIVE + 1)/*** @def MAX_PRIM* Defines the max minor number of the primary partitions.* If there are 2 disks, prim_dev ranges in hd[0-9], this macro will* equal 9.*/
#define MAX_PRIM            (MAX_DRIVES * NR_PRIM_PER_DRIVE - 1)
#define MAX_SUBPARTITIONS   (NR_SUB_PER_DRIVE * MAX_DRIVES)

在这本书中,只考虑硬盘接在主IDE通道的情况,所以最多支持两块硬盘,因此MAX_DRIVES定义为2。NR_SUB_PER_PART定义的是每个扩展分区最多有多少个逻辑分区。根据NR_PART_PER_DRIVE的值容易算出NR_PRIM_PER_DRIVE为5,它其实表示的是hd[0~4]这5个分区,因为有些代码中我们把整块硬盘(hd0)和主分区(hd[1~4])放在一起看待。MAX_PRIM定义的是主分区的最大值,比如有两块硬盘,那第一块硬盘的主分区为hd[1~4],第二块硬盘的主分区为hd[6~9],所以MAX_PRIM为9,我们定义的hd1a的设备号应大于它,这样通过与MAX_PRIM比较,我们就可以知道一个设备是主分区还是逻辑分区。

主设备号的情况要简单一些,因为它的作用在于找到相应的驱动程序,所以我们只要建立一个以主设备号为下标、以驱动器号(PID)为值的数组,就可以了。具体如下面代码所示。

代码 kernel/global.c,dd_map。

/*** For dd_map[k], * 'k' is the device nr.\ dd_map[k].driver_nr is the driver nr.* * Remeber to modify include/const.h if the order is changed.*/
struct dev_drv_map dd_map[] = {/* driver nr.                       major device nr. *//* ----------                       ---------------- */{INVALID_DRIVER},                   /**< 0 : Unused */{INVALID_DRIVER},                   /**< 1 : Reserved for floppy driver */{INVALID_DRIVER},                   /**< 2 : Reserved for cdrom driver  */{TASK_HD},                          /**< 3 : Hard disk */{TASK_TTY},                         /**< 4 : TTY */{INVALID_DRIVER}                    /**< 5 : Reserved for scsi disk driver */
};

主设备号的定义如下,代码 include/const.h。

/* major device numbers (corresponding to kernel/global.c::dd_map[]) */
#define NO_DEV          0
#define DEV_FLOPPY      1
#define DEV_CDROM       2
#define DEV_HD          3
#define DEV_CHAR_TTY    4
#define DEV_SCSI        5/* make device number from major and minor numbers */
#define MAJOR_SHIFT     8
#define MAKE_DEV(a,b)   ((a << MAJOR_SHIFT) | b)/* separate major and minor numbers from device number */
#define MAJOR(x)        ((x >> MAJOR_SHIFT) & 0xFF)
#define MINOR(x)        (x & 0xFF)#define INVALID_DRIVER  -20

结构体 dev_drv_map 的定义在 include/fs.h 中,这是新增加的一个头文件。

struct dev_drv_map {int driver_nr; /**< The proc nr.\ of the device driver. */
};

一定要注意,主设备号的宏定义的值为dd_map[]的下标,两者是相呼应的,若要改变的话要同时改变。将来我们每个设备号都有主设备号和次设备号组成,通过简单的位运算即可得到主设备号及次设备号。

刚才我们在给磁盘映像hd.img分区时,指定hd.img5为Orange’s分区,我们将来会把文件系统建立在这个分区上。根据我们的命名规则,它的名字应该是hd2a。它的次设备号应该等于hd1a加上16。

用代码遍历所有分区

好了,分区表的原理已经清楚了,下面我们就来添加代码,在硬盘驱动程序中找出所有分区并且将它们打印出来。

代码 kernel/hd.c,读取分区表。

PRIVATE struct hd_info hd_info[1];#define DRV_OF_DEV(dev) (dev <= MAX_PRIM ? dev / NR_PRIM_PER_DRIVE : (dev - MINOR_hd1a) / NR_SUB_PER_DRIVE)/* task_hd */
/* Main loop of HD driver */
PUBLIC void task_hd()
{
...switch (msg.type) {case DEV_OPEN:hd_open(msg.DEVICE);break;
...}...
}/*** <Ring 1> Check hard drive, set IRQ handler, enable IRQ and initialize data structures.*/
PRIVATE void init_hd()
{
...int i;for (i = 0; i < sizeof(hd_info) / sizeof(hd_info[0]); i++) {memset(&hd_info[i], 0, sizeof(hd_info[0]));}hd_info[0].open_cnt = 0;
}/*** <Ring 1> This routine handles DEV_OPEN message. It identify the drive * of the given device and read the partition table of the drive if it * has not been read.* * @param device The device to be opend.*/
PRIVATE void hd_open(int device)
{int drive = DRV_OF_DEV(device);assert(drive == 0); /* only one drive */hd_identify(drive);if (hd_info[drive].open_cnt++ == 0) {partition(drive * (NR_PART_PER_DRIVE + 1), P_PRIMARY);print_hdinfo(&hd_info[drive]);}
}/*** <Ring 1> Get a partition table of a drive.* * @param drive     Drive nr (0 for the 1st disk, 1 for the 2nd, ...)n* @param sect_nr   The sector at which the partition table is located.* @param entry     Ptr to part_ent struct.*/
PRIVATE void get_part_table(int drive, int sect_nr, struct part_ent * entry)
{struct hd_cmd cmd;cmd.features = 0;cmd.count = 1;cmd.lba_low = sect_nr & 0xFF;cmd.lba_mid = (sect_nr >> 8) & 0xFF;cmd.lba_high = (sect_nr >> 16) & 0xFF;cmd.device = MAKE_DEVICE_REG(1, /* LBA mode */drive, (sect_nr >> 24) & 0xF);cmd.command = ATA_READ;hd_cmd_out(&cmd);interrupt_wait();port_read(REG_DATA, hdbuf, SECTOR_SIZE);memcpy(entry, hdbuf + PARTITION_TABLE_OFFSET, sizeof(struct part_ent) * NR_PART_PER_DRIVE);
}/*** <Ring 1> This routine is called when a device is opened. It reads the * partition table(s) and fills the hd_info struct.* * @param device Device nr.* @param style  P_PRIMARY or P_EXTENDED.*/
PRIVATE void partition(int device, int style)
{int i;int drive = DRV_OF_DEV(device);struct hd_info * hdi = &hd_info[drive];struct part_ent part_tbl[NR_SUB_PER_DRIVE];if (style == P_PRIMARY) {get_part_table(drive, drive, part_tbl);int nr_prim_parts = 0;for (i = 0; i < NR_PART_PER_DRIVE; i++) { /* 0~3 */if (part_tbl[i].sys_id == NO_PART) {continue;}nr_prim_parts++;int dev_nr = i + 1; /* 1~4 */hdi->primary[dev_nr].base = part_tbl[i].start_sect;hdi->primary[dev_nr].size = part_tbl[i].nr_sects;if (part_tbl[i].sys_id == EXT_PART) { /* extended */partition(device + dev_nr, P_EXTENDED);}}assert(nr_prim_parts != 0);} else if (style == P_EXTENDED) {int j = device % NR_PRIM_PER_DRIVE; /* 1~4 */int ext_start_sect = hdi->primary[j].base;int s = ext_start_sect;int nr_1st_sub = (j - 1) * NR_SUB_PER_PART; /* 0/16/32/48 */for (i = 0; i < NR_SUB_PER_PART; i++) {int dev_nr = nr_1st_sub + i; /* 0~15/16~31/32~47/48~63 */get_part_table(drive, s, part_tbl);hdi->logical[dev_nr].base = s + part_tbl[0].start_sect;hdi->logical[dev_nr].size = part_tbl[0].nr_sects;s = ext_start_sect + part_tbl[1].start_sect;/* no more logical partitions in this extended partition */if (part_tbl[1].sys_id == NO_PART) {break;}}} else {assert(0);}
}/*** <Ring 1> Print disk info.* * @param hdi Ptr to struct hd_info.*/
PRIVATE void print_hdinfo(struct hd_info * hdi)
{int i;for (i = 0; i < NR_PART_PER_DRIVE + 1; i++) {printl("%sPART_%d: base %d(0x%x), size %d(0x%x) (in sector)\n",i == 0 ? " " : "     ",i,hdi->primary[i].base,hdi->primary[i].base,hdi->primary[i].size,hdi->primary[i].size);}for (i = 0; i < NR_SUB_PER_DRIVE; i++) {if (hdi->logical[i].size == 0) {continue;}printl("         %d: base %d(0x%x), size %d(0x%x), (in sector)\n",i,hdi->logical[i].base,hdi->logical[i].base,hdi->logical[i].size,hdi->logical[i].size);}
}/*** <Ring 1> Get the disk information.* * @param drive Drive Nr. */
PRIVATE void hd_identify(int drive)
{struct hd_cmd cmd;cmd.device = MAKE_DEVICE_REG(0, drive, 0);cmd.command = ATA_IDENTIFY;hd_cmd_out(&cmd);interrupt_wait();port_read(REG_DATA, hdbuf, SECTOR_SIZE);print_identify_info((u16*)hdbuf);u16* hdinfo = (u16*)hdbuf;hd_info[drive].primary[0].base = 0;/* Total Nr of User Addressable Sectors */hd_info[drive].primary[0].size = ((int)hdinfo[61] << 16) + hdinfo[60];
}

在之前的代码中,驱动程序收到DEV_OPEN消息之后调用函数hd_identify(),在这里我们改成了调用函数hd_open(),这是新加的一个函数,它接受的参数即为设备的次设备号。在hd_open()中,我们首先由设备次设备号得到驱动器号,由于我们的Bochs只定义了一个硬盘,所以这里的驱动器号一定是0。接下来便是调用hd_identify()了。再往下是一个if语句,其中涉及我们新定义的一个结构体:hd_info。它的定义如下代码所示。

struct part_ent {u8 boot_ind;        /*** boot indicator*   Bit 7 is the active partition flag,*   bits 6-0 are zero (when not zero this*   byte is also the drive number of the*   drive to boot so the active partition*   is always found on drive 80H, the first*   hard disk).*/u8 start_head;      /*** Starting Head*/u8 start_sector;    /*** Starting Sector.*   Only bits 0-5 are used. Bits 6-7 are*   the upper two bits for the Starting*   Cylinder field.*/u8 start_cyl;       /*** Starting Cylinder.*   This field contains the lower 8 bits*   of the cylinder value. Starting cylinder*   is thus a 10-bit number, with a maximum*   value of 1023.*/u8 sys_id;      /*** System ID* e.g.*   01: FAT12*   81: MINIX*   83: Linux*/u8 end_head;        /*** Ending Head*/u8 end_sector;      /*** Ending Sector.*   Only bits 0-5 are used. Bits 6-7 are*   the upper two bits for the Ending*    Cylinder field.*/u8 end_cyl;     /*** Ending Cylinder.*   This field contains the lower 8 bits*   of the cylinder value. Ending cylinder*   is thus a 10-bit number, with a maximum*   value of 1023.*/u32 start_sect; /*** starting sector counting from* 0 / Relative Sector. / start in LBA*/u32 nr_sects;       /*** nr of sectors in partition*/};struct part_info {u32 base; /* # of start sector (NOT byte offset, but SECTOR) */u32 size; /* how many sectors in this partition */
};/* main drive struct, one entry per drive */
struct hd_info
{int open_cnt;struct part_info primary[NR_PRIM_PER_DRIVE];struct part_info logical[NR_SUB_PER_DRIVE];
};

与此同时我们声明了一个数组:hd_info[1],鉴于目前我们的虚拟机只装了一块硬盘,我们只给了它一个成员。hd_info的主要作用是记录硬盘的分区信息,每个硬盘应有一个hd_info结构。其中primary成员用来记录所有主分区的起始扇区和扇区数目,它们占用primary[1-4],logical用来记录所有逻辑分区的起始扇区和扇区数目。注意这里整个硬盘的起始扇区和扇区数目记在了primary[0]中。

我们接着来看hd_open,其中的if语句判断hd_info的open_cnt成员是否为0,并将其自加。由于在init_hd()中我们将结构体清零了,所以第一次执行到这里时if判断为真,于是调用partition()和print_hdinfo()。

函数partition()所做的便是获取硬盘分区表了,这个过程我们已经清楚了,这里只不过是用C语言代码写出来而已。注意其中的读硬盘扇区的工作封装在了函数get_part_table()中,和执行IDENTIFY命令类似,执行READ命令时我们同样是先填充hd_cmd结构,然后交给hd_cmd_out()来写寄存器。

函数print_hdinfo()就比较简单了,将获取的分区信息打印出来而已。

这里有一点需要说明一下,上一篇文档的代码FS发送DEV_OPEN消息时没有任何附加参数,现在hd_open()是带参数的了,所以FS的代码也要修改一下。

代码 fs/main.c,修改后的文件系统进程。

/*** <Ring 1> The main loop of TASK FS.*/
PUBLIC void task_fs()
{printl("Task FS begins.\n");/* open the device: hard disk */MESSAGE dirver_msg;dirver_msg.type = DEV_OPEN;dirver_msg.DEVICE = MINOR(ROOT_DEV);assert(dd_map[MAJOR(ROOT_DEV)].driver_nr != INVALID_DRIVER);send_recv(BOTH, dd_map[MAJOR(ROOT_DEV)].driver_nr, &dirver_msg);spin("FS");
}

这里我们不仅将ROOT_DEV的次设备号通过消息发送给了驱动程序,而且使用哪个驱动程序也变成由dd_map来选择,这样一来,只要将ROOT_DEV定义好了,正确的消息便能发送给正确的驱动程序。ROOT_DEV的定义如下所示。

代码 include/const.h,ROOT_DEV。

/* device numbers of hard disk */
#define MINOR_hd1a      0x10
#define MINOR_hd2a      (MINOR_hd1a + NR_SUB_PER_PART)#define ROOT_DEV        MAKE_DEV(DEV_HD, MINOR_BOOT)

其中MINOR_BOOT被定义成MINOR_hd2a,放在一个新的头文件config.h中,将来一些硬盘配置的宏定义将放在这个文件中。

代码 include/config.h。

#define MINOR_BOOT          MINOR_hd2a

好了,现在FS会把hd2a的次设备号发给dd_map[DEV_HD].driver_nr,即TASK_HD——我们的硬盘驱动,然后驱动程序将执行hd_open,从而获取硬盘的分区信息。现在我们可以make并执行看一下效果了。提醒一下,由于我们新增加了头文件,所以不要忘记更改Makefile。运行效果如下图所示。

好了,做了这么多准备工作,硬盘的分区信息总算是打印出来了。

欢迎关注我的公众号

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

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

相关文章

python 工作目录 与 脚本所在目录不一致

工作目录&#xff1a;执行脚本的地方 我以为工作目录会是当前执行脚本的目录位置&#xff0c;但其实不是&#xff0c;例如&#xff1a; 图中红色文件为我执行的脚本文件&#xff0c;但是实际的工作目录是PYTHON LEARNING 可以用如下代码查询当前工作目录&#xff1a; import os…

dubbo的springboot集成

1.什么是dubbo&#xff1f; Apache Dubbo 是一款 RPC 服务开发框架&#xff0c;用于解决微服务架构下的服务治理与通信问题&#xff0c;官方提供了 Java、Golang 等多语言 SDK 实现。使用 Dubbo 开发的微服务原生具备相互之间的远程地址发现与通信能力&#xff0c; 利用 Dubbo …

【三】把Python Tk GUI打包exe可执行程序,移植到其他机器可用

背景 这是一个系列文章。上一篇【【二】为Python Tk GUI窗口添加一些组件和绑定一些组件事件-CSDN博客】 使用python脚本写一个小工具。因为命令行运行的使用会有dos窗口&#xff0c;交互也不是很方便&#xff0c;开发环境运行也不方便分享给别人用&#xff0c;所以想到…

ubantu中的docker安装

1.Ubuntu Docker 安装 | 菜鸟教程 (runoob.com) 我就是看这个教程进行操作的 2.执行下面两步&#xff0c;就算是安装完成了 3.启动&#xff0c;并检查是否安装成功&#xff1a; 4.安装之后&#xff0c;怎么用&#xff0c;那就是自己随便探索咯&#xff0c;可以看博客&#xf…

3D Web可视化开发工具包HOOPS Communicator:提供Web端浏览大型模型新方案!

前言&#xff1a;HOOPS Communicator是Tech Soft 3D旗下的主流产品之一&#xff0c;具有强大的、专用的高性能图形内核&#xff0c;专注于基于Web的高级3D工程应用程序。其由HOOPS Server和HOOPS Web Viewer两大部分组成&#xff0c;提供了HOOPS Convertrer、Data Authoring的模…

【Spring Cloud】Gateway组件的三种使用方式

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Spring Cloud》。&#x1f3af;&#x1f3af; &am…

游戏版 ChatGPT,要用 AI 角色完善生成工具实现 NPC 自由

微软与 AI 初创公司 Inworld 合作&#xff0c;推出基于 AI 的角色引擎和 Copilot 助理&#xff0c;旨在提升游戏中 NPC 的交互力和生命力&#xff0c;提升游戏体验。Inworld 致力于打造拥有灵魂的 NPC&#xff0c;通过生成式 AI 驱动 NPC 行为&#xff0c;使其动态响应玩家操作…

蜗牛目标检测数据集VOC格式480张

蜗牛&#xff0c;一种缓慢而坚韧的软体动物&#xff0c;以其螺旋形的外壳和黏附力极强的黏液而为人所熟知。 蜗牛体型呈螺旋形&#xff0c;有一个硬壳保护其柔软的身体。壳的形状和纹理因种类而异&#xff0c;有的光滑如玻璃&#xff0c;有的则布满细纹。蜗牛的头部有两对触角…

现代 C++ 及 C++ 的演变

C 活跃在程序设计领域。该语言写入了许多新项目&#xff0c;而且据 TIOBE 排行榜数据显示&#xff0c;C 的受欢迎度和使用率位居第 4&#xff0c;仅次于 Python、Java 和 C。 尽管 C 在过去二十年里的 TIOBE 排名都位居前列&#xff08;2008 年 2 月排在第 5 名&#xff0c;到…

el-table实现多行合并的效果,并可编辑单元格

背景 数据为数组包对象&#xff0c;对象里面有属性值是数组&#xff1b;无需处理数据&#xff0c;直接使用el-table包el-table的方法&#xff0c;通过修改el-table的样式直接实现多行合并的效果 html代码 <template><div><el-table size"mini" :d…

【Cadence】sprobe的使用

实验目的&#xff1a;通过sprobe测试电路中某个节点的阻抗 这里通过sprobe测试输入阻抗&#xff0c;可以通过port来验证 设置如下&#xff1a; 说明&#xff1a;Z1代表sprobe往left看&#xff0c;Z2代表sprobe往right看 结果如下&#xff1a; 可以看到ZM1I0.Z2 顺便给出了I…

基于GPT4+Python近红外光谱数据分析及机器学习与深度学习建模

详情点击链接&#xff1a;基于ChatGPT4Python近红外光谱数据分析及机器学习与深度学习建模教程 第一&#xff1a;GPT4基础 1、ChatGPT概述&#xff08;GPT-1、GPT-2、GPT-3、GPT-3.5、GPT-4模型的演变&#xff09; 2、ChatGPT对话初体验&#xff08;注册与充值、购买方法&am…

酒店客房管理系统设计与实现(代码+数据库+文档)

&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目 希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;一、研究背景 1.1 研究背景 当…

N9914A FieldFox 手持式射频分析仪,6.5 GHz

N9914A FieldFox 手持式射频分析仪 简述&#xff1a; Keysight FieldFox 便携式分析仪可以在非常恶劣的工作环境中&#xff0c;轻松完成从日常维护到深入故障诊断的各项工作。 选择最适合您需求且有强大软件支持的 Keysight FieldFox 配置。 FieldFox 分析仪可配置为电缆与天线…

C语言中的指针变量p,特殊表达式p[0] ,(*p)[0],(px+3)[2] ,(*px)[3]化简方法

一.已知以下代码&#xff0c;请问以下 式子p[0] &#xff0c;p[1] &#xff0c;(*p)[0] &#xff0c;(*p)[1] 是什么意思&#xff1f; int A[3] {1,2,3}; int (*p)[3] &A; 因为前面的嵌入式C语言基础的章节中说过&#xff0c;数组下标其实就是数组首元素的地址往上偏…

【学术会议】第三届神经计算青年研讨会 学习笔记

第三届神经计算青年研讨会 学习笔记 会议时间&#xff1a;2024-1-6至2024-1-7 会议地点&#xff1a;电子科技大学 会议介绍&#xff1a; 为提升我国神经计算⻘年研究队伍的学术⽔平和国际影响⼒&#xff0c;研讨会主题涵盖&#xff1a;神经系统建模与模拟、脑机接⼝与类脑智能、…

探索Java中最常用的框架:Spring、Spring MVC、Spring Boot、MyBatis和Netty

目录 前言 Spring框架 Spring MVC框架 Spring Boot框架 MyBatis框架 Netty框架 结语 作者简介&#xff1a; 懒大王敲代码&#xff0c;计算机专业应届生 今天给大家聊聊探索Java中最常用的框架&#xff1a;Spring、Spring MVC、Spring Boot、MyBatis和Netty&#xff0c;希…

程序员离职后,居然发现一个绝佳的私活渠道...

今天&#xff0c;给大家推荐一些用Python爬虫做私活的渠道&#xff01; 先给各位还不熟悉Python爬虫的朋友介绍一下&#xff01; 可以短时间获得大量资料~ 可以进一步数据分析 当然也可以获得收益&#xff01; 学会Python爬虫以后&#xff0c;还可以通过各种渠道&#xff08;…

SAP 获取物料/批次/订单的特性值(学习一)

1、事务码 MSC1N、MSC2N、MSC3N 2、常用表 MCH1、MCHA、AUSP、MCH*开头的几个 3、批次 1、创建批次 BAPI&#xff1a;BAPI_BATCH_CREATE 2、修改批次 BAPI&#xff1a;BAPI_BATCH_CHANGE 3、删除批次 BAPI&#xff1a;BAPI_BATCH_DELETE 4、获取批次明细 BAPI&…

23种设计模式精讲,配套23道编程题目 ,支持 C++、Java、Python、Go

关于设计模式的学习&#xff0c;大家应该还是看书或者看博客&#xff0c;但却没有一个边学边练的学习环境。 学完了一种设计模式 是不是应该去练一练&#xff1f; 所以卡码网 针对 23种设计&#xff0c;推出了 23道编程题目&#xff0c;来帮助大家练习设计模式&#xff0c;地…