C++とLinuxのlibmountでファイルシステムの一時的なマウントをする

ファイルシステムの一時的なマウント操作を伴う処理をプログラムで行う場合は確実な後片付けへの配慮が要求される

2021年8月23日 嶋田大貴

恒久的にマウントされているわけではないファイルシステムリソース(NFSやリムーバブルメディア、仮想マシンイメージなど)に対して行う操作をプログラムで自動化する場合、当然ながら

という順序で処理を行うわけだが、1番目に失敗した場合はともかくとして 2番目の処理の途中で想定していないエラーなどによりプログラムが終了した場合はかなりの確率で後々面倒なことになる。特に無人で定期実行されるジョブではアンマウントされていないリソースが蓄積することで致命的な障害につながりかねない。

なので、2番めの処理が正常に行われようが何か失敗しようがなるべく確実に3番目の処理(ファイルシステムのアンマウント)を呼び出したい。記述言語が C++の場合に、ラムダを使用してそれを実現するのが下記の例となる。C++をそのような用途に使う機会が普通あるかどうかは置いておく。

シグナルまで考慮する必要があれば with_tempmount 関数呼び出しの前後で適宜sigprocmack(2)などすること。

/**
 * LinuxとC++でファイルシステムの一時的なマウントをやりやすくするやつ
 */

#include <filesystem>
#include <functional>

#include <unistd.h>
#include <libmount/libmount.h>

void with_tempmount(
    const std::filesystem::path& device, const char* fstype, int flags, const char* data,
    std::function<void(const std::filesystem::path&)> func)
{
    struct libmnt_context *ctx = mnt_new_context();
    if (!ctx) throw std::runtime_error("mnt_new_context failed");

    auto path = std::filesystem::temp_directory_path() /= std::string("tempmount-") + std::to_string(getpid());
    std::filesystem::create_directory(path);
    try {
        mnt_context_set_fstype_pattern(ctx, fstype);
        mnt_context_set_source(ctx, device.c_str());
        mnt_context_set_target(ctx, path.c_str());
        mnt_context_set_mflags(ctx, flags);
        mnt_context_set_options(ctx, data);
        auto rst = mnt_context_mount(ctx);
        auto status = mnt_context_get_status(ctx);
        auto helper_success = mnt_context_helper_executed(ctx) == 1 ? (mnt_context_get_helper_status(ctx) == 0) : true;
        mnt_free_context(ctx);
        if (rst != 0) throw std::runtime_error("mnt_context_mount failed");
        if (status != 1) throw std::runtime_error("mnt_context_get_status returned error");
        if (!helper_success) throw std::runtime_error("mnt_context_get_helper_status returned error");
        try {
            func(path);
        }
        catch (...) {
            umount(path.c_str());
            throw;
        }
        umount(path.c_str());
    }
    catch (...) {
        std::filesystem::remove(path);
        throw;
    }
    std::filesystem::remove(path);
}

#ifdef __MAIN__
// テスト用の main関数
// g++ -std=c++20 -D__MAIN__ with_tempmount.cpp -lmount
#include <iostream>

int main(int argc, char* argv[])
{
    if (geteuid() != 0) {
        // Must be root user to perform mount
        std::cerr << "You must be a root user." << std::endl;
        return 1;
    }
    try {
        with_tempmount("none", "tmpfs", MS_RELATIME, "rw", [](const auto& path) {
            std::cout << "Temporarily mounted tmpfs dir is '" << path.string() 
                << "'. This filesystem will automatically be unmounted right after the scope ends." << std::endl;
            // throw std::runtime_error("Even if exception occur.");
        });
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return 1;
    }
    return 0;
}
#endif

このプログラムでは mount(2) を使用せずに mnt_context_* なる関数群 を使用してファイルシステムのマウントを行っている。これは libmountというライブラリによって提供される関数で、これを使用することでヘルパーを必要とする特殊なマウントにも対応できる。たとえば、ループバックマウントは mountコマンド-o loop を与えることで行えることがよく知られているが、その裏では「ループバックデバイスを作成する」という処理が自動的に行われている。実は POSIX由来で情報豊富かつ呼び出しの簡単な mount(2)ではそのような暗黙の処理をしてはくれないため、ループパックデバイスやヘルパーを必要とするファイルシステムのマウントをプログラムから行うには代わりに libmountを使用する必要がある。libmountの使用例を示す記事などは世の中にほぼ無いので代わりに util-linuxのソースなどを参照のこと(sys-utils/mount.cあたり)

2021年8月23日 嶋田大貴

記事一覧へ戻る