linux下给一个文件设置文件系统

前言

在linux下,我们总有说磁盘分区给自己使用的需求,这里我们来通过给一个文件来进行分区等操作为例子,整体来梳理一下分区,建立文件系统,挂载等操作来梳理下这一整个流程。除此之外,我们给一个文件设置分区也有他的用处:1. 如果你需要在磁盘上多分一个区出来,但是没有可用的空间了,这时你可以使用文件来代替一个新的分区设备使用。2. 在制作镜像的时候,也可以使用这种方式,划分分区,建立文件系统后将镜像所需要的所有文件拷贝进去,进而制作成iso或者qcow2等格式的镜像文件供使用。

规划一下

输入结构

我们使用python3来完成这个程序,这里我们输入文件名,分区信息,文件系统类型等,程序中我们根据文件名建立一个打的文件,然后设置他的分区信息,之后在分区的基础上建立文件系统,最后我们在里边建立几个文件作为测试使用。最后就输出了这个文件,我们把这个文件挂载后,查看最终的信息等。

{
    "filename": "mul_part_file.raw",
    "pttype": "gpt",
    "size": 100,
    "size_type": "M", // G,M,K,B
    "partitions": [
        {
            "size": 40,
            "filesystem" : {
                "fs_type": "xfs",
                "mountpoint": "/",
                "label": "root_fs"
            }
        },
        {
            "size": 0,
            "filesystem" : {
                "fs_type": "ext4",
                "mountpoint": "/to_use/",
                "label": "another_fs"
            }
        }
    ]
}

由于信息比较多,我们用一个json结构来传递给我们的程序,以上为我们的例子。指定了最终的名字,分区表的类型关于分区,可以参照我之前的文章。我们也指定了文件的大小和以什么单位来计算,partitions里边分别指定了每个分区的信息,包括文件系统信息和大小等等。

代码结构

我们首先要明确的是,如果磁盘(这里也是把这个文件模拟成磁盘)到达使用状态,需要进行一下几个步骤(摘自《鸟哥Linux私房菜-基础篇》):

  1. 对磁盘进行分区,以建立可用的 partition ;
  2. 对该 partition 进行格式化 (format),以建立系统可用的 filesystem;
  3. 若想要仔细一点,则可对刚刚建立好的 filesystem 进行检验;
  4. 在 Linux 系统上,需要建立挂载点 (亦即是目录),并将他挂载上来

而我们本篇内容也是模拟这整个过程。
通过输入的结构我们知道,需要分区表,分区,文件系统的类,这也是最基本的。除此之外我们也需要提供mount,mkfs等实操函数,那接下来我们就来看下我们代码:

实现

def uuid_gen():
    return str(uuid.uuid4())

# class PartitionTable
class PartitionTable:
    def __init__(self, label, partitions):
        self.label = label # mbr or gpt
        self.uuid = uuid_gen()
        self.partitions = partitions or []

    def write_patations(self, targetFile):
        """Write the partition table to disk"""
        # generate the command for sfdisk to create the table
        command = f"label: {self.label}\nlabel-id: {self.uuid}"
        for partition in self.partitions:
            fields = []

            for field in ["start", "size", "type", "name", "uuid"]:
                value = getattr(partition, field)
                fields += [f'{field}="{value}"']
            command += "\n" + ", ".join(fields)

        subprocess.run(["sfdisk", "-q", targetFile],
                       input=command,
                       encoding='utf-8',
                       check=True)

    def partitions_with_filesystems(self) -> List[Partition]:
        """Return partitions with filesystems sorted by hierarchy"""
        def mountpoint_len(p):
            return len(p.filesystem.mountpoint)
        parts_fs = filter(lambda p: p.filesystem is not None, self.partitions)
        return sorted(parts_fs, key=mountpoint_len)

首先我们来看PartitionTable,基本的就是label,uuid以及自己包含的整个分区结构,这里我们支持gpt和mbr分区,用label表示。write_patations函数就是向文件中写入分区结构,使用sfdisk命令来操作,指定了每个分区的start,size,type,name,uuid。而partitions_with_filesystems函数就是将分区系统按照其中mount路径的长短来排序输出。

为啥分区文件系统要排序输出?后边会说到

# class Partition
class Partition:
    def __init__(self, start: int = None,
                 size: int = None,
                 name: str = "",
                 filesystem: Filesystem = None):
        self.start = start
        self.type = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
        self.size = size
        self.uuid = uuid_gen()
        self.filesystem = filesystem
        self.name = name

    @property
    def start_in_bytes(self):
        return (self.start or 0) * 512

    @property
    def size_in_bytes(self):
        return (self.size or 0) * 512

然后看下partition结构,这里start,size等为例sfdisk使用方便,都是以扇区为单位的,所以就加了两个属性start_in_bytes和size_in_bytes,以字节为单位计算的start和size的大小。
大家仔细看type="0FC63DAF-8483-4772-8E79-3D69D8477DE4"这一坨又是个啥?
这个是分区的时候指定之后这个分区时做什么用的类型的GUID,那都有那些可以使用呢?
在这里插入图片描述
这里给出大家供参考:分区表GUID

我们只是使用最基本的,所以使用Linux filesystem data就好。大家可能也许会说,使用fsdik/gdisk并没有看到这个呀?我来给大家找一下:

$ gdisk /dev/[your device]
Command (? for help): ?
...()
   l	list known partition types
...()
Command (? for help): l
...()     
7f01 ChromeOS root         7f02 ChromeOS reserved     8200 Linux swap          
8300 Linux filesystem      8301 Linux reserved        8302 Linux /home         
8303 Linux x86 root (/)    8304 Linux x86-64 root (/  8305 Linux ARM64 root (/)
8306 Linux /srv            8307 Linux ARM32 root (/)  8400 Intel Rapid Start   
8e00 Linux LVM             a000 Android bootloader    a001 Android bootloader 2
...()

$ fdisk /dev/[yout device]
Command (m for help): m
...()
   l	list known partition types
...()
Command (m for help): l
...()
 1  FAT12           27  Hidden NTFS Win 82  Linux swap / So c1  DRDOS/sec (FAT-
 2  XENIX root      39  Plan 9          83  Linux           c4  DRDOS/sec (FAT-
 3  XENIX usr       3c  PartitionMagic  84  OS/2 hidden or  c6  DRDOS/sec (FAT-
 4  FAT16 <32M      40  Venix 80286     85  Linux extended  c7  Syrinx         
 5  Extended        41  PPC PReP Boot   86  NTFS volume set da  Non-FS data    
 6  FAT16           42  SFS             87  NTFS volume set db  CP/M / CTOS / .
 7  HPFS/NTFS/exFAT 4d  QNX4.x          88  Linux plaintext de  Dell Utility   
 8  AIX             4e  QNX4.x 2nd part 8e  Linux LVM       df  BootIt
 ...()

gdisk中的0x8300,fdisk中的0x83就是我们上边所说的type的省略。

def mkfs_ext4(device, uuid, label):
    opts = []
    if label:
        opts = ["-L", label]
    subprocess.run(["mkfs.ext4", "-U", uuid] + opts + [device],
                   input="y", encoding='utf-8', check=True)


def mkfs_xfs(device, uuid, label):
    opts = []
    if label:
        opts = ["-L", label]
    subprocess.run(["mkfs.xfs", "-m", f"uuid={uuid}"] + opts + [device],
                   encoding='utf-8', check=True)
# class Filesystem
class Filesystem:
    def __init__(self,
                 fstype: str,
                 mountpoint: str,
                 label: str = None):
        self.type = fstype
        self.uuid = uuid_gen()
        self.mountpoint = mountpoint
        self.label = label

    def make_at(self, device: str):
        fs_type = self.type
        if fs_type == "ext4":
            maker = mkfs_ext4
        elif fs_type == "xfs":
            maker = mkfs_xfs
        else:
            raise ValueError(f"Unknown filesystem type '{fs_type}'")
        maker(device, self.uuid, self.label)

然后就是我们的文件系统了,我这里只写了支持ext4和xfs两种文件系统。make文件系统的时候根据参数区调用不同的命令。
比较基本的结构我们已经讲述完了,接下来把整个代码的结构串起来:

if __name__ == '__main__':
    options = json5.load(sys.stdin)
    main(options)

入口函数我们通过标准输入来接收参数,转化为dict格式,调用main函数:

def parse_size(origin_size, size_type):
    if size_type == "G":
        size = origin_size * 1024 * 1024 * 1024
    elif size_type == "M":
        size = origin_size * 1024 * 1024
    elif size_type == "K":
        size = origin_size * 1024
    else:
        size = origin_size

    if size % 512 != 0:
        raise ValueError("`size` must be a multiple of sector size (512)")

    return size

def main(options):
    filename = options.get("filename", "temp.raw")
    size_type = options.get("size_type", "B")
    size =  parse_size(options.get("size", 0), size_type)

    # Create an empty file
    subprocess.run(["truncate", "--size", str(size), filename], check=True)
    
    # ...

main函数得到filename,size_type和size,通过size_type把size转为字节为单位的。接下里创建一个size大小的文件。

def main(options):
# ...
	
    # write partations
    pt = partition_table_from_options(options, size, size_type)
    pt.write_patations(filename)

然后就是解析出来我们的整个基本结构,写入分区。
先简单看下解析过程吧:

def partition_table_from_options(options, total_size, size_type) -> PartitionTable:
    pttype = options.get("pttype", "dos")
    partitions = options.get("partitions", [])

    if pttype == "mbr":
        pttype = "dos"

    if partitions is None:
        raise ValueError("partitions is none")

    def filesystem_from_json(js):
        return Filesystem(js["fs_type"], js["mountpoint"], js.get("label"))

    def partition_from_json(start, size, filesystem):
        return Partition((int)(start / 512), (int)(size / 512), "", filesystem_from_json(filesystem))

    start = 2048 * 512
    parts = []
    free_size = total_size - start
    if pttype == "gpt":
        free_size -= 33 * 512

    for p in partitions:
        size = parse_size(p.get("size", 0), size_type)
        if size > free_size or size == 0:
            size = free_size

        parts.append(partition_from_json(start, size, p.get("filesystem")))
        start = start + size
        free_size -= size

    return PartitionTable(pttype, parts)

代码看起来比较长,主要都是解析工作,这里我们要提一个的是,我们之前文章*讲到,分区第一个扇区从2048扇区开始,另外GPT分区最后的33个LBA(可以看作扇区)会用来备份分区表的信息。所以最开始的2048和最后33个扇区不能使用。在此基础上我们尽量满足每个分区的size请求。另外还有就是Partition要接收start和size以扇区为单位,所以要除以512(filesystem_from_json函数)

到这里我们设置完了分区,main函数中继续往下看:

@contextlib.contextmanager
def setup_loop_device(image):
    try:
        device = ""
        r = subprocess.run(["losetup", "-P", "-f", "--show", image], stdout=subprocess.PIPE, check=False, encoding="UTF-8")
        if r.stdout and r.stdout.strip():
            device = str(r.stdout.strip())
        
        if device:
            subprocess.run(["partprobe", "-s", f"{device}"], check=True)
        
        yield device
    finally:
        if device:
            subprocess.run(["losetup", "-d", f"{device}"], check=True)

            
def main(options):
# ...

	# set filesystems and mount
    with contextlib.ExitStack() as cm:
        root = cm.enter_context(tempfile.TemporaryDirectory(dir="/tmp", prefix="leap-tmp-"))
        device = cm.enter_context(setup_loop_device(filename))
        if not device:
            return

这里contextlib.ExitStack()是python中的一个装饰器。使用cm.enter_context包装的对象,会先执行其enter函数,将其压栈,当with执行完,会弹出栈中对象,执行其exit函数。其实就是提供了类似c++执行外作用域后的析构函数的功能。也可以使用@contextlib.contextmanager来装饰函数,eg. setup_loop_device: 在cm.enter_context时,会先去执行setup_loop_device函数到yield处,获取到返回值,with完成后会执行setup_loop_device中finally中的内容

tempfile.TemporaryDirectory即为创建一个临时文件,用它作为我们大文件的根目录,一会儿我们会用到它。
我们要给文件设置文件系统,较为普遍的做法时,先将其绑定到loop设备上,loop设备是一个伪设备,可以用来模拟块设备,然后对该loop设备进行操作,最后解绑就好,这里的操作就会应用到文件。不过也不排除一些可以直接操作大文件的做法。这里我们还是演示较为普遍的做法吧。

然后我们跳到setup_loop_device函数去:使用losetup将我们的文件和loop绑定,如果系统有可用loop设备,返回后拿到,刷新一下返回设备给调用者。等到全部执行完之后losetup -d解绑。

@contextlib.contextmanager
def mount(source, dest):
    subprocess.run(["mount", source, dest], check=True)
    try:
        yield dest
    finally:
        subprocess.run(["umount", "-R", dest], check=True)

def main(options):
# ...

	# set filesystems and mount
    with contextlib.ExitStack() as cm:
        root = cm.enter_context(tempfile.TemporaryDirectory(dir="/tmp", prefix="leap-tmp-"))
        device = cm.enter_context(setup_loop_device(filename))
        if not device:
            return
        
        device_no = 1
        for partition in pt.partitions_with_filesystems():
            sub_device = f"{device}p{device_no}"
            device_no += 1

            partition.filesystem.make_at(sub_device)

            # now mount it
            mountpoint = os.path.normpath(f"{root}/{partition.filesystem.mountpoint}")
            os.makedirs(mountpoint, exist_ok=True)
            cm.enter_context(mount(sub_device, mountpoint))
            
        // copy xxx to root

拿到绑定的loop设备,比如说是/devloop0,我们文件有两个分区,那么就会映射处来两个子设备/dev/loop0p1和/dev/loop0p2,然后我们就是建立文件系统。在刚刚创建临时文件夹中创建mount路径。这里我们要回答下上编提到的partitions_with_filesystems函数中文件系统排序的问题,由短到长,主要是因为考虑到子目录的关系,我们创建文件的时候/root/比/root/abc这个短,那么肯定先创建/root/文件夹,然后再创建/root/abc文件夹,后边挂载也是一样的道理。

然后就是挂载到相应的文件夹,这里挂载其实意义不是很大,其实考虑的是如果会向这个大文件中拷贝内容的话,那么一定是要挂载出来目录才可以,但是我们没有拷贝文件,只是做到一个演示作用。

这个执行完后就会先写在刚刚的挂载目录,解绑出来loop设备,删掉临时文件夹

校验

这里我们就在一个文件中创建了文件系统,我们通过命令看下:

$ sfdisk -l mul_part_file.raw
Disk mul_part_file.raw: 100 MiB, 104857600 bytes, 204800 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: ABC13258-55C4-49D8-B8A7-CF8EBB79DA89

Device             Start    End Sectors Size Type
mul_part_file.raw1  2048  83967   81920  40M Linux filesystem
mul_part_file.raw2 83968 204766  120799  59M Linux filesystem

展示了分区的信息。
然后我们把他挂载起来:

$ sudo losetup -P -f --show /home/zhangdexin/review/osbuild/mul_part_file.raw
/dev/loop0

$ lsblk
NAME      MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0       7:0    0   100M  0 loop 
├─loop0p1 259:0    0    40M  0 loop 
└─loop0p2 259:1    0    59M  0 loop 

$ sudo mount /dev/loop0p1 /mnt/core/
$ ls /mnt/core/
to_use

$ df -Th /mnt/core/
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/loop0p1   xfs    35M  2.4M   33M   7% /mnt/core

$ sudo mount /dev/loop0p2 /mnt/core/to_use/
$ df -Th /mnt/core/to_use/
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/loop0p2   ext4   54M  1.3M   48M   3% /mnt/core/to_use

$ sudo umount /mnt/core/to_use/
$ sudo umount /mnt/core/
$ sudo losetup -d /dev/loop0

还是使用losetup,之后我们看到产生了loop0p1和loop0p2,分别挂载loop0p1和loop0p2,这里我们也看到他们的文件系统分别是xfs和ext4。

总结

以上就是我么全文了,我们看到使用一个大文件在其中创建文件系统这个过程,帮助大家梳理了一块磁盘到使用时的每个步骤。同时我们也可以使用大文件来做一个分区给我们在磁盘没有多余空间分区时使用。同时也再次恭喜大家看,这个流程也是制作镜像的一部分,不管时qcow2,iso或者img镜像都是这些固定流程,只是如果要启动的话,会把内容bootloader和rootfs拷贝到大文件中。

欢迎大家讨论。

ref

https://linux.die.net/man/8/sgdisk
https://linux.die.net/man/8/sfdisk
https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
https://wiki.archlinux.org/title/GPT_fdisk
https://github.com/osbuild/osbuild
https://github.com/kata-containers/osbuilder/tree/master/image-builder
《鸟哥linux私房菜-基础篇》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值