前言
在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私房菜-基础篇》):
- 对磁盘进行分区,以建立可用的 partition ;
- 对该 partition 进行格式化 (format),以建立系统可用的 filesystem;
- 若想要仔细一点,则可对刚刚建立好的 filesystem 进行检验;
- 在 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私房菜-基础篇》