使用librosa实现视频素材音乐卡点

背景

近期业务上有一个需求:需要将我们已有的一些视频素材作为数据源,结合有节奏感的BGM,输出一个能够卡点的视频素材。类似于这样的功能在很多视频剪辑软件如剪映、快影中都能够看到,它们会让用户上传多个视频,然后再让用户挑选一个喜欢的BGM,就可以快速输出一个卡点好的视频。我们利用librosa简单实现了这样的功能,并结合后台的一系列自动化工具,使用户能够快速得到大量的具有音乐卡点效果的视频素材,我们将该能力与现有系统相结合,提升了混剪视频制作效率并降低了制作成本。

本篇文章去除了业务上的一些细节,将如何利用开源工具快速实现音乐卡点功能做了总结整理。

使用到的开源工具

OpenCV:OpenCV(开源计算机视觉库)是一个开源计算机视觉和机器学习软件库。可以用于图像处理、视频处理、特征提取等很多场景。这里我们用它来获取视频帧率、尺寸、帧,并用于视频的帧级别的裁剪。

spleeter:spleeter是一个源分离器。可以用于音频的分离,将人声、鼓、贝斯和其他声音分离开来,便于后续的声音处理。

librosa:librasa是一个用于音乐和音频分析的 python 包。可以用于提取音频的采样值、采样率、节拍时间点等信息。

AudioCraft:AudioCraft 是一个用于音频生成深度学习研究的 PyTorch 库。 其中它的 MusicGen 功能可以用于生成音乐。

AudioSegment:AudioSegment 可以用于音频文件的裁剪。

整体逻辑

从整体逻辑看,整个流程不算复杂。共有如下几个步骤:

  1. 确定合适的数据源,也就是素材物料库,可以从原始物料中来,也可以从素材成片来。
  2. 离线将大量的视频做好切幕以及分类。
  3. 将上述分幕按照一定的规则导入到剪辑服务中。
  4. 剪辑服务输出成品卡点视频的地址,以供用户使用。

实现流程

BGM来自于成熟曲库

一般像剪映以及快影“剪同款”中的“卡点”功能就都算是BGM来自于成熟曲库。因为曲库中的音乐该在什么时刻进行卡点都已经是生成好了的。

后台需要做的事情仅就是将导入的多个视频裁剪为适合拼接的视频即可。这种卡点方式的实现流程比较简单。

  1. 接收用户传入的多个视频,此处校验视频时长是否满足需求。
  2. 导入音乐的卡点方案,这个卡点方案如果是时刻的数组,则可以使用 numpy 转为时间间隔的数组。如:将[1.345, 2.345, 3.456, 4.567, 5.678]转为[1.345, 1, 1.111, 1.111, 1.111]。便于后续视频的裁剪。
  3. 选择视频并进行视频裁剪,此处的视频可能是与卡点间隔一一对应的,也可能是随机或依据某个规则选择的。可以使用OpenCV进行视频裁剪,它可以实现帧级别的处理,非常适合卡点这种对时间要求较高的场景。

用OpenCV做视频裁剪的示例代码如下:

def video_stretch(input_file, output_file, dt):
    cap = cv2.VideoCapture(input_file)
    fps = cap.get(cv2.CAP_PROP_FPS)
    (w, h) = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    # 读取 video
    video = []
    success = 1
    while success:
        success, frame = cap.read()
        if success:
            video.append(frame)
    cap.release()
    # 选择片段
    frame_cnt = int(dt * fps)
    new_fps = int(dt * fps) / dt
    start = int((len(video) - frame_cnt) / 2)
    sub_video = video[start:(start + frame_cnt)]
    # 保存到新视频
    vision_writer = cv2.VideoWriter(output_file, cv2.VideoWriter_fourcc(*'xvid'), new_fps, (w, h))
    for frame in sub_video:
        vision_writer.write(frame)
    vision_writer.release()

这里裁剪的片段是取的中间的片段,实际上也可以在此处加入算法,判断从何处截取视频。

  1. 使用 ffmpeg 做视频的拼接,示例代码如下:
concat_cmd = "concat:" + "|".join(
            [f"{workspaceMix}/bgm_stuck/tmp_ts/{scen_list_idx}_{i}.ts" for i in range(len(choose_list))])
cmd = 'ffmpeg -y -i \\"{concat_cmd}\\" -i "{audio_path}" -vcodec copy -absf aac_adtstoasc {output_file}'
os.system(cmd)

其中,{workspaceMix} 是剪辑机器上的工作空间。{scen_list_idx}_{i}.ts 是待拼接的视频片段。

ffmpeg 的拼接支持的比较简单,如果需要在视频间增加转场特效,还需要另外调用相应能力。

BGM来自于AI生成或用户上传

这种场景与上述BGM来自于成熟曲库的场景相比,多了一个在音乐中标记卡点时刻的步骤。而这一步的实现还是比较有挑战的,如果标记的不合理,那么最终呈现出的卡点效果就会很差。目前看起来剪映和快影还没有支持该功能,或许也是因为出片质量不是很好保障吧。

这里仅介绍一下AI音乐生成以及对应音乐的卡点时刻标记。用户上传音乐场景类似。

  1. AI生成音乐使用的是 AudioCraft 的 MusicGen 功能,我们只需要输入一段 Prompt,就可以得到相应的音乐。比如,我用的Prompt为:cheerful music without drums。可以得到如下音乐:
  1. 利用 Separator 将音乐做音轨分离,提取出音乐的纯粹节拍部分。代码如下:
sp = Separator(params_descriptor="spleeter:4stems")
sp.separate_to_file(orig_path, music_ai_path, codec="wav")

其中,参数params_descriptor="spleeter:4stems"表示将该段音频分离为人声、鼓、贝斯和其他声音。spleeter 还有一些别的参数可供选择。详细可参考:https://research.deezer.com/projects/spleeter.html

  1. 使用 librosa 加载鼓点音频文件,并获取到对应的音频时序数组以及采样率。代码如下:
drums, sampling_rate = librosa.load(drum_path)

其中,drum_path 是鼓点音频文件的路径,drumssampling_rate分别表示音频时序数组以及采样率,示例分别为:[0.0012345, 0.0023456, 0.0034567, …] 和 44100。

  1. 使用 librosa 获取鼓点音频的节拍时刻。代码如下:
estimated_bpm, beats = librosa.beat.beat_track(y=drums, sr=sampling_rate, units='time')

其中drumssampling_rate分别表示前面获取到的音频时序数组以及采样率。units 表示节拍位置的单位,可选time和frames,time 表示以时间为单位,frames 表示以帧数为单位。

estimated_bpm表示估计的全局节奏,以每分钟节拍为单位。beats是节拍时刻的数组。

librosa.beat.beat_track 具体的检测算法可以参考文档:http://labrosa.ee.columbia.edu/projects/beattrack/

  1. 使用 librosa 获取得到脉冲信号所在帧,使用上述 beats 计算得到节奏所在帧,并将脉冲信号所在帧与节奏所在帧进行匹配,得到节奏点。代码如下:
# 获取得到脉冲信号所在帧
onset_env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512, aggregate=np.median)
peaks = librosa.util.peak_pick(onset_env, 1, 1, 1, 1, 0.8, 5)
# 创建一个节拍值1/4、2/4、3/4、4/4的数组
M = beats * [[1 / 4], [2 / 4], [3 / 4]]
M = M.flatten()
M = np.sort(M)
# 局部脉冲与节拍点做10%的去误差,得到节奏点
L = []
for i in M:
	for j in peaks:
		if i * 0.9 < j < i * 1.1:
	    L.append(j)
L = list(set(L))
L.sort()
# 取前20个点,不够20个则全取
if len(L) > 20:
	point_list = librosa.frames_to_time(L[:20], sr=sr)
else:
	point_list = librosa.frames_to_time(L[:len(L)], sr=sr)

这里我们有个假设,就是AI生成或用户上传的音乐一定是2/4拍或3/4拍或4/4拍中的一种。这样才能通过该方法来找到节奏点。最终输出的节奏点是以脉冲信号为准的,因为有可能音乐的节奏点没有脉冲信号,我们要的卡点时刻一定是要有脉冲信号的。

librosa.util.peak_pick 的具体检测算法可以参考文档:https://librosa.org/doc/main/generated/librosa.util.peak_pick.html

  1. 使用AudioSegment 进行音频文件的裁剪,具体怎么裁剪这个就看业务需求了,参考代码如下:
# 音乐裁剪,设置开始结束时间
end_time = point_list[len(point_list) - 1] + start_time
start_time = start_time * 1000
end_time = end_time * 1000
sound = AudioSegment.from_mp3(music_path)
word = sound[start_time:end_time]
# 音乐储存路径
word.export('movie/music.wav', format="wav")
  1. 将节奏点时刻数组作为卡点方案,以及待剪辑的视频作为输入。这样后续的流程就和“BGM来自于成熟曲库”的流程一样了。

总结

本文总结了如何利用一些开源工具快速的实现视频素材BGM卡点的功能。限于篇幅,有很多细节都没有涉及到,比如更多酷炫转场的实现、相似素材场景聚类的实现等。后续如果有机会,会继续续写分享。

相关参考资料

OpenCV文档:https://docs.opencv.org/4.x/d9/df8/tutorial_root.html

spleeter:https://research.deezer.com/projects/spleeter.html

librosa:https://librosa.org/doc/latest/index.html

AudioCraft:https://audiocraft.metademolab.com/

AudioSegment:https://pydub.com/

虚拟人的快速实现:语音驱动图片

背景

由于业务需要,近期调研了如何使用开源工具快速的实现一个虚拟人,这个虚拟人来自于一个图片,我们需要做的是要让这个图片动起来,且图中人物的口型能够跟对应的语音对应上。为此,我了解几个开源的工具或模型,最终选择使用SadTalker来实现该功能。

本文是对SadTalker的使用以及其用到的基础模型做一总结。希望能够对想要实现该功能的同学有所帮助。

要达成的目标

需要有个开源工具,能够实现如下的输入输出,效果要尽可能的好,并且方便我们将该服务本地化。

输入:一段文字脚本、一个音色模型、一张人物图片

输出:一个视频,该视频中的人物能够开口说话,并且用指定的音色说出文字脚本的内容

SadTalker相关信息

Github地址:https://github.com/OpenTalker/SadTalker

hugging face地址:https://huggingface.co/spaces/vinthony/SadTalker

SadTalker的使用

关于SadTalker的使用,其官方文档已经写的比较全面了,这里不再赘述。只提一下其中一些参数的使用要点。

  1. 如果要用preprocess的full参数,则一定要加上–still参数,不然效果会很糟糕。
  2. free-view Mode参数可以用于控制头像的转动,其中参数的详细含义如下:

input_yawinput_pitch 和 input_roll 通常用于描述物体或相机在三维空间中的旋转角度,也被称为欧拉角或俯仰、偏航和翻滚角。

  • input_yaw 表示绕垂直于地面的轴旋转的角度,也称为偏航角。通常以正北方向为0度,顺时针方向为正方向,逆时针方向为负方向。
  • input_pitch 表示绕水平轴旋转的角度,也称为俯仰角。通常以水平面为0度,向上旋转为正方向,向下旋转为负方向。
  • input_roll 表示绕前后轴旋转的角度,也称为翻滚角。通常以垂直于水平面的轴为0度,向右旋转为正方向,向左旋转为负方向。
  1. 就我目前来看,如果我们需要头部动起来,那么还是不设置旋转参数的效果更好。

SadTalker使用到的模型

模型介绍

shape_predictor_68_face_landmarks.dat

shape_predictor_68_face_landmarks.dat是一个基于dlib库的人脸关键点检测模型文件,可以用于检测人脸的68个关键点,包括眼睛、眉毛、鼻子、嘴巴等部位的位置信息。这个模型文件在很多人脸识别、表情识别、人脸姿态估计等领域都有广泛的应用。(基于随机森林的回归算法)

Deep3DFaceReconstruction

**Deep3DFaceReconstruction**是一种基于深度学习的人脸三维重建技术。它通过利用深度学习算法对人脸图像进行分析和处理,从而实现对人脸的三维重建。这种技术可以广泛应用于计算机视觉、虚拟现实、增强现实等领域,为人们带来更加逼真的视觉体验。

Deep3DFaceReconstruction的核心技术是利用深度学习算法对人脸图像进行分析和处理,从而提取出人脸的三维信息。具体来说,它通过对大量的人脸图像进行训练,学习到了一种高效的特征提取方法,可以快速准确地提取出人脸的关键特征,包括面部轮廓、眼睛、鼻子、嘴巴等。然后,它通过对这些特征进行三维重建,从而得到了一个高度逼真的人脸三维模型。

Deep3DFaceReconstruction具有许多优点,例如可以实现快速高效的三维重建、可以处理不同角度和光照条件下的人脸图像、可以处理不同种族和性别的人脸图像等。此外,它还可以应用于人脸识别、人脸动画、人脸表情识别等领域,具有广泛的应用前景。

auido2pose_00140-model.pth

auido2pose_00140-model.pth是一个训练好的PyTorch模型文件,用于音频到人体姿态的转换。该模型可以根据输入的音频数据,预测出对应的人体姿态信息。在这个文件中,auido2pose_00140-model.pth表示训练过程中的第140个epoch。在每个epoch中,模型会对训练数据进行一次完整的训练,以更新模型参数。通常情况下,训练的epoch次数越多,模型的预测效果会越好,但是也会增加训练时间和计算资源的开销。该模型文件可以被应用于很多领域,如虚拟现实、运动分析、人体姿态识别等。(PoseVAE模型,基于transformer模型的)

auido2exp_00300-model.pth

auido2exp_00300-model.pth是一个训练好的模型文件,用于将音频数据转换为对应的面部表情。该模型通过分析音频数据和面部表情数据之间的关系,可以预测出对应的面部表情信息。auido2exp_00300-model.pth可以被应用于很多领域,如虚拟现实、面部表情识别、情感分析等。通过分析音频数据和面部表情数据之间的关系,该模型可以实现对面部表情的快速、准确的预测。(ExpNet模型,基于transformer模型的)

wav2lip.pth

wav2lip.pth是一个基于深度学习的语音和口型同步技术的模型文件,可以将音频和视频中的语音和口型同步,生成逼真的合成视频。该模型使用了深度学习中的卷积神经网络和循环神经网络等技术,实现了对音频和视频的特征提取和匹配。wav2lip.pth可以被应用于很多领域,如虚拟现实、视频编辑、电影制作等。通过将音频和视频进行同步,可以实现更加逼真的人机交互和视频合成效果,提高用户体验。(基于循环神经网络)

mapping_00109-model.pth.tar

mapping_00109-model.pth.tar的作用是将输入音频中学习到的逼真的3D运动系数转换为对应的基础向量,从而生成一个逼真的3D人脸。具体来说,mappingNet模型学习从显式的3DMM运动系数(头部姿态和表情)到隐式的基础向量之间的关系,并将这个关系应用于输入音频中学习到的逼真的3D运动系数,从而生成对应的基础向量。最后,这个基础向量与3DMM模型中的基础向量相结合,生成一个逼真的3D人脸。(基于GAN神经网络)

facevid2vid_00189-model.pth.tar

facevid2vid_00189-model.pth.tar是一个基于深度学习的人脸动作转换模型,可以将输入的人脸视频转换成指定动作的人脸视频。简单来说,可以将上述3D模型视频转为穿了图片皮肤的视频。(基于GAN神经网络)

GFPGANv1.4.pth

GFPGANv1.4.pth是一种图像超分辨率重建模型,用于将低分辨率图像(LR)提升至高分辨率图像(HR)。它是由腾讯优图实验室提出的一种基于生成对抗网络(GAN)的方法,可以在不失真的情况下提高图像质量。GFPGANv1.4.pth是该模型的一个预训练权重文件,可以用于对新的低分辨率图像进行重建。该模型可以应用于许多领域,如数字娱乐、医学影像、安防监控等。(基于GAN神经网络)

处理流程中的相关模型

  1. 对原始图片进行裁剪,这一步用到shape_predictor_68_face_landmarks.dat算法模型识别出人脸,并使用CV2进行裁剪处理。
  2. 将原始图片中的人脸识别成一个3D人脸模型系数,并存储在mat格式的文件中。
  3. 3DMM提出给定视频中的眨眼动作。(将视频中的人脸识别成一个3D人脸模型系数,并存储在mat格式的文件中。)
  4. 3DMM提出给定视频中的姿势动作。(将视频中的人脸识别成一个3D人脸模型系数,并存储在mat格式的文件中。)
  5. 从音频中提取出姿势、表情以及唇形,并存储在mat格式的文件中。
  6. 使用上述系数渲染出一个3D人脸模型,并用ffmpeg命令将音频与视频做结合。
  7. 将视频与图片做结合。
  8. 效果增强,使视频变得更加清晰。

模型处理的时延测试

音频时长:10秒

处理总时间:366054毫秒(6分钟)

模型处理时间总占比:97.20%

测试机器配置:半张NVIDIA Tesla T4显卡、显存:8GB

处理过程模型处理时间(单位:毫秒)处理时间占比
图片的预处理shape_predictor_68_face_landmarks.dat
epoch_20.pth92962.54%
眨眼视频的处理shape_predictor_68_face_landmarks.dat
epoch_20.pth4094811.19%
姿势视频的处理shape_predictor_68_face_landmarks.dat
epoch_20.pth300448.21%
音频提出系数auido2exp_00300-model.pth
auido2pose_00140-model.pth
wav2lip.pth10940.30%
3D人脸模型渲染mapping_00109-model.pth.tar12231233.41%
视频与图片做结合facevid2vid_00189-model.pth.tar66461.82%
效果增强gfpgan14544939.73%

可以看到,模型处理过程中,最耗时的是3D人脸模型渲染以及效果增强,两者耗时占比超70%。

SadTalker的部署与服务

SadTalker的项目代码是用Python编写的,我们可以用FastAPI很快速的把它改造为能够通过HTTP对外提供服务的方式,并且在服务器中用uvicorn启动该服务。这样就可以在自己的本地环境中稳定的提供服务了,在有代码变更的情况下,也无需手动重启服务。

我们的服务还与腾讯云COS存储打通了,语音和图片从COS中读取,并且将处理成功的视频文件写入到COS中,并将链接地址返回。

总结

本文是对SadTalker的使用以及其用到的基础模型做了一下总结,其中包括了模型的介绍以及各模型处理时长的试验结果,但并没有涉及到模型的原理以及优化细节。对于针对于业务的优化,可以后续视情况来续写分享。

参考资料

SadTalker Github地址:https://github.com/OpenTalker/SadTalker

SadTalker hugging face地址:https://huggingface.co/spaces/vinthony/SadTalker

SadTalker最佳实践:https://github.com/OpenTalker/SadTalker/blob/main/docs/best_practice.md

FastAPI:https://github.com/tiangolo/fastapi

Python中的if __name__==’__main__’

背景

初学者在学习Python的过程中,一定都遇到过if __name__ == '__main__'这样的语句。那么,这样的一个语句有什么作用呢?在什么时候需要使用这个判断语句呢?

举个例子

执行:

def main():
    print "hello world!"
    print __name__

if __name__ == '__main__':
 main()

print __name__

文件名为:tesy.py
执行结果为:
在这里插入图片描述
可以看到,直接执行该模块,__name__变量为__main__
执行:

import test

文件名为:test1.py
执行结果为:
在这里插入图片描述
可以看到,将test作为模块导入另一个文件中,得到的结果为test。
这是因为在导入test模块后,也会执行该模块。当__name____main__时才会执行main()。
然而test作为模块导入另一个文件时,__name__为模块名:test。因此最后只打印了一个test。

语句的作用

我们应该都知道类似__xxx__这种形式的变量是python的内置变量,在声明用户变量时不可以与内置变量重复。而这个__name__内置变量代表的是模块名(模块也就是python文件)。该模块名在直接运行模块时为__main__,在作为模块导入时为模块本身名称。
所以,这个语句的作用就是:当该模块直接运行时执行if__name__=='__main__'中的语句。当该模块作为模块导入到别的文件中时,因为__name__不为__main__,于是这个判断中的语句不能被执行。

使用U盘传数据时操作系统做了什么(源码分析)

一、背景

学习linux文件系统时考虑一个具体的问题:我们经常会用U盘传输东西到计算机中。当我们把U盘插入到一台计算机上,并且从U盘中复制文件到计算机里,然后卸载U盘,将U盘拔出。操作系统在这一连串过程中做了些什么?这篇博客仅从文件系统的角度利用linux内核源码浅析该过程。本篇博客使用的linux内核版本为3.10.0。

二、文件系统基础

1.文件系统

在开始之前我想先简单介绍一下文件系统的基础知识。
我们都知道,计算机磁盘被抽象成了一个个块设备,一个块作为一个存储单元。我的系统块大小为1KB,linux中可以使用df命令来查看块大小。
在块抽象上还有另一层抽象,主要由三部分组成:超级块、i节点表、数据块。每个部分都由若干的块来组成。当然,数据块占用最多的块。
这里写图片描述
图中展示了这三个部分。其中引导块我不在这里详述。i节点也叫i-node,也可以叫做索引节点。叫法不同,实际指同一对象。
以EXT4文件系统为例,接下来我对这三个区域分别进行介绍:

超级块

超级块用来描述整个文件系统本身的信息。每个具体的文件系统如:EXT2、EXT3、EXT4、NTFS,它们都拥有自己的超级块。各自的超级块描述着各自文件系统的情况。
这里给大家展示EXT4文件系统的超级块在linux内核中的定义(部分):
这里写图片描述
EXT4文件系统的超级块定义存放在/fs/ext4文件夹下的ext4.h中

i节点

i节点存储于i节点表中。i节点中存储着大量的关于文件的重要信息,如文件的各种属性,文件的数据存储的位置等。
EXT4文件系统i节点在内核中的定义(部分):
这里写图片描述
EXT4文件系统的i节点定义存放在/fs/ext4文件夹下的ext4.h中

数据区

数据区,顾名思义,存储的是文件的具体数据。每个文件占用多个块来进行存储。通过i节点来指向这些数据块的位置。
当一个文件“静静”的在磁盘上时,i节点用来描述这个文件,每个文件对应一个i节点号,这个i节点号之于文件就相当于身份证号之于我们公民。i节点中存储着关于文件的各种属性,并且i节点也描述了文件数据的存储位置。
那么,现在有个问题。
当我们打开一个文件,需要读取文件的属性或者文件的数据时操作系统是怎么做的?当然这个文件的信息需要被读取到内存中进行处理。首先,我们需要知道我们有没有权限打开这个文件,那么肯定就要查看该文件i节点中存储的属性信息。因为文件权限就是文件的一个属性。所以该文件的i节点的信息需要被读取到内存中。
各种不同的文件系统的文件都可能被操作系统打开,所以i节点信息都可能需要读取到内存中。面对不同的文件系统,操作系统如何进行统一的管理就成了问题。由此,操作系统引入了一种抽象的文件系统,这种抽象的文件系统可以用同一组系统调用对各种不同的文件系统以及文件进行操作。这种抽象的文件系统被称作虚拟文件系统。

2.虚拟文件系统(VFS)

虚拟文件系统相对于具体的如EXT2、EXT3、NTFS这样的文件系统不同之处在于“虚拟”二字。所以要理解虚拟文件系统,重点在于理解“虚拟”。
首先,虚拟文件系统在磁盘中是不占用存储空间的,它只活动于内存之中。对于一个具体的如EXT4文件系统,当用户需要使用该文件系统时就会将有关信息存入内存之中,即使用虚拟文件系统来进行操作。
如果内核要使用一个文件系统,第一步要做的是什么?当然是将该文件系统的超级块信息调入虚拟文件系统的超级块中。

VFS超级块

那么,我们来看一下虚拟文件系统中超级块的模样(部分):
这里写图片描述
这些信息存储在/include/linux/fs.h中,很明显,这个目录存储的不是某一具体的文件系统的信息。
之前我们说过,文件的i节点信息在未使用时“静静”的躺在磁盘中,当打开该文件,则需要使用相关信息。这时,文件的i节点信息会被调入内存中,填充的就是VFS的i节点。

VFS i节点

看一下虚拟文件系统中i节点结构的模样(部分):
这里写图片描述
这些信息同样存储在/include/linux/fs.h中。

dentry结构

实际上,一个文件除了被i节点描述,还被一个叫做dentry的结构所描述。在这里大家可能困惑,我之前说过一个文件是被一个i节点所描述的,这里却又说被一个dentry结构所描述,这不是自相矛盾了么?
实际上并不矛盾。我之前说文件被i节点描述指的是在具体的文件系统当中,指的是当文件“静静”躺在磁盘上时是被i节点所描述的。但对于虚拟文件系统,一个文件除了被i节点描述,还被dentry描述。而二者描述的角度是不同的。i节点描述了文件的固有属性,如文件大小、文件权限等。dentry描述了文件的逻辑属性,比如它的父目录是什么。
我们知道一个文件可以创建多个硬链接,硬链接的实质是不同的文件名指向同一个i节点,实际上这些不同的文件名的“本质”是一样的。所以,它们的i节点是相同的。然而,这些文件名可能处于不同的父目录中,那么它们的逻辑属性就不相同了,也就是说dentry是不同的。
对于i节点与dentry的区别,可以举个例子:
对于同一个出版社出版的某一图书,比如同济版的高数书,它有其固有属性如页数、版面大小、字数多少。对应i节点来描述。但同一本书可以出版多本,全国各地可能都有此书,那么,这些书分布的省份、书店、学校,这些就是逻辑属性。对应dentry来描述。
我们看一下dentry在内核中的定义:
这里写图片描述
在i节点的结构体中,有一个dentry的链表。
实际上,一个i节点可以对应多个dentry。

file结构

file结构是文件对象,linux在file结构中保存了文件打开的位置,称为打开的文件描述。这是一个与进程相关的文件结构,许多进程打开文件时对于文件的描述在其中被定义。其中具体的定义可查看源码中的定义:
这里写图片描述
定义在/include/linux/Fs.h中。

三、过程演示

使用mount命令
这里写图片描述
我们可以看到我的linux文件系统是EXT4,而U盘的文件系统是NTFS。
使用cp命令从U盘中复制一张图片到我的家目录下。
这里写图片描述
可以看到这个图片名叫0.png
结果当然是家目录下多了一个文件0.png
这里写图片描述

四、源码分析

上述演示过程看起来非常简单,只是使用cp命令指定一个文件和一个目录而已。
问题在于从我将U盘插入到传输数据再到我将U盘拔出。这期间操作系统做了哪些工作呢?
我的linux文件系统是EXT4,U盘的文件系统是NTFS。我们知道,我们将U盘插入计算机时,U盘会进行一个注册与安装的过程,也就是挂载。我的U盘挂载在了/run/media目录下。所以我的U盘的数据都可以从此文件夹下找到。

文件系统的注册、安装、卸载

那么,注册在源码中是如何实现的?

注册

内核在编译时就确定了其支持的文件系统,这些文件系统在系统启动时就在VFS中进行了注册。注册在内核中的实现是填写一个叫做file_system_type的结构体。该结构体中定义了文件系统类型名,文件系统特征,超级块的函数指针等。而且还定义了一个用于指向下一个file_system_type结构体的指针。
这里写图片描述
该结构体在/include/linux/fs.h中被定义。
如果需要安装的文件系统在linux中并没有注册呢?如果是内核可加载的模块,那么文件系统就会在实际安装的时候进行注册,而在卸载的时候进行注销。
实际上,该结构体的信息并不完整,我们都知道,要挂载一个文件系统,必须要指定一个挂载点,而这里并没有指明挂载点位于哪里。这就涉及下一个步骤,安装。

安装

文件系统的安装必须指定一个安装点。该安装点位于根文件系统的任一目录下。我的U盘安装在了/run/media下。把一个文件系统安装到一个安装点要用到的主要结构为mount
这里写图片描述
该结构体在/fs的Mount.h文件中被定义。

卸载

卸载文件系统就是将该文件系统的相关结构体从链表中释放。而在此之前,必须保证VFS中的超级块不为“脏”,而且没有该文件系统中的文件正在使用。卸载的内核实现是sys_umount。内核中源码位于fs/Namespace.c中,这里就不再详细介绍了。

不同文件系统间数据的传输

对于我上面演示的cp /run/media/pf/pf/0.png /home/pf命令,简单抽象的执行过程是这样的:
1.打开/run/media/pf/pf/0.png文件
2.在/home/pf文件下中新建0.png文件
3.读取/run/media/pf/pf/0.png文件数据到缓冲区
4.从缓冲区将数据写入/home/pf/0.png
5.关闭这两个文件
我们知道,我相互传数据的这两个文件系统是不同的,那么,不同的文件系统之间为什么可以用同一个系统调用来进行操作呢?
答案是linux的虚拟文件系统,那么内核中具体是如何实现的?
我这里以读取/run/media/pf/pf/0.png文件数据到缓冲区为例进行内核源码的分析。
首先,当用了read()系统调用,会进入内核态调用sys_read()。这个函数的实现源码如下:
这里写图片描述
我们可以看到,这里使用了宏定义SYSCALL_DEFINE3,这样就可以传递变长参数。这里的重点执行函数在于vfs_read()。也就是说接下来会调用这个函数继续执行操作。我们进入其定义。
这里写图片描述
前面三条都是判断语句。重点在于后面的判断语句:如果file->f_op->read不存在则执行do_sync_read()函数,若存在,则执行file->f_op->read()函数。
从这里可以很显然的看到,file与f_op都是结构体,file中定义了f_op,f_op中定义了read。现在,来看一下file结构体的样子。
这里写图片描述
之前在基础章描述过它,这里不再重复描述。这里的重点是要找到f_op。
我们可以看到它是一个file_operations类型的结构体。
file_operations想必大家都知道,这是一个文件操作的结构体,其中定义的都是文件操作的函数,也就是接口。内核中源码实现如下:
这里写图片描述
这个结构体也位于/include/linux/Fs.h中。
想必现在大家就明白了,f_op调用的read()其实就是这个结构中定义的函数read。但是,这里的read并没有具体的实现。对于不同的文件系统,它们可能就有不同的实现方案。例如我linux的EXT4文件系统对于file_operations的实现与我U盘NTFS文件系统对于file_operations的实现就不相同。
EXT4文件系统file_operations结构的具体实现:
这里写图片描述
定义在/fs/ntfs/File.c中。
NTFS文件系统file_operations结构的具体实现:
这里写图片描述
定义在/fs/ext4/File.c中。
可以看到,它们对于文件读、写等实现函数是相同的,但是对于有些函数如release、open等,实现函数则是不同的。
到此,大家可能就明白了linux文件系统是如何实现相同的系统调用实现对于不同文件系统的操作。实际上核心就是使用了file_operation这个接口实现的。

五、总结

实际上,在linux中有许多非常巧妙的设计,而且这些巧妙的设计也确实有其必然性。重要的是要学会透过现象看本质,抓住一般规律。本篇博客对于linux文件系统的分析还不算深入,今后还会进一步进行深入学习。