XFS는 예전 커널에서는 패치를 적용하여 사용할 수 있었지만 2.4.25 이후 버전의 2.4 커널과 2.6 대의 커널에서는 정식으로 커널 소스 트리에 포함되었다. 여기에서는 가장 최신 버전의 안정 커널인 2.6.10에 포함된 XFS 소스코드를 가지고 살펴본다.

XFS의 특징
XFS는 SGI에서 개발한 고성능 저널링 파일 시스템으로 64비트 주소를 지원하며 확장성 있는 자료 구조와 알고리즘을 사용한다. XFS의 대표적인 특징을 살펴보면 다음과 같다.

<화면 1> SGI 의 XFS 프로젝트 홈페이지

◆ 저널링(신속한 복구 기능) : XFS의 저널링 기법을 사용하여 파일 수에 관계없이 예상치 못한 상황으로부터 신속하게 복구하여 재시작이 가능하다. 기존의 저널링을 사용하지 않는 파일 시스템의 경우에는 이러한 일을 수행하기 위해 오랜 시간에 걸쳐 파일 시스템 체크 프로그램을 수행해야만 했었다. XFS는 이러한 체크 프로그램을 사용하지 않는다.

◆ 신속한 트랜잭션 : XFS는 저널링 기법의 장점을 제공하면서도 데이터 읽기/쓰기 트랜잭션으로 인한 성능 저하를 최소화한다. XFS의 저널링 구조와 알고리즘은 트랜잭션에 대한 로그 기록을 신속하게 할 수 있도록 최적화되어 있다.

◆ 높은 확장성 : XFS는 완전한 64비트 파일 시스템이기 때문에 100만 테라바이트 크기의 파일도 다룰 수 있다(263 = 8 x 1018 = 8 exa-bytes = 8,000,000 tera-bytes). 100만 테라바이트는 현재 사용되고 있는 가장 큰 파일 시스템이 처리할 수 있는 것보다 10만 배나 더 큰 것이다. 이것은 극히 큰 주소 공간인 것 같지만, 요즘 디스크 크기의 발전 추세에 비추어 볼 때 근래에 유용하게 쓰이게 될 것이다.

디스크 공간이 커짐에 따라 단지 주소 공간만이 커질 것이 아니라 그에 따른 자료 구조나 알고리즘도 같이 확장되어야 한다. XFS는 이러한 확장성을 제공하는 준비된 파일 시스템이다.

◆ 뛰어난 처리량 : XFS는 거의 raw IO 성능에 가까운 성능을 낼 수 있는 파일 시스템이다. XFS는 수 GB/s의 성능을 내는 SGI MIPS 시스템에서 테라바이트 단위의 파일 시스템 확장성을 검증받았다. 리눅스가 엔터프라이즈 영역에서 차지하는 비중이 날로 커지고 있으며, 리눅스 서버의 처리량이 증가함에 따라 XFS이 리눅스에서도 이러한 양의 데이터를 신속히 처리할 수 있게 된다.

XFS의 디스크 구조
할당 그룹
XFS는 할당 그룹(Allocation Group)이라는 단위로 나눠진다. 각각의 할당 그룹은 독립적으로 존재하며 병렬적으로 처리된다. 파일 시스템을 생성할 때 할당 그룹의 크기와 수를 지정할 수 있는데 이를 지정하지 않은 경우에는 기본적으로 주어진 디스크를 8등분하여 8개의 할당 그룹을 생성한다.

각각의 할당 그룹의 0번 블럭에는 슈퍼 블럭 정보가 유지되고 그 다음 블럭들에는 할당 그룹 헤더 정보가 존재한다. 마운트 시에는 첫 번째 할당 그룹의 슈퍼 블럭만을 사용하며 나머지는 슈퍼 블럭의 데이터가 깨친 상황에서 응급 복구 시에 사용된다. XFS의 슈퍼 블럭 구조체는 에 xfs_sb_t로 정의되어 있다.

할당 그룹 헤더 정보는 free space를 관리하는 xfs_agf_t, 아이노드 정보를 관리하는 xfs_agi_t, free list를 관리하는 xfs_agfl_t라는 3개의 구조체의 형태로 나눠져 있다. 이들 정보가 올바르게 유지되고 있는지를 관리하기 위해 메모리 상에 xfs_perag_t라는 구조체를 유지한다. xfs_agf_t 구조체는 free space를 관리하기 위해 B+ 트리를 유지하는 데 동일한 정보를 각각 블럭 번호(bno)와 블럭 크기(cnt)를 키 값으로 가지게 하여 필요에 따라 더 효율적인 처리가 이뤄지도록 하였다.

아이노드
XFS에서 관리하는 아이노드 구조체는 파일에 xfs_inode_t로 정의되어 있다. 디스크 상에 직접 기록되는 아이노드의 구조는 에 정의된 xfs_dinode_t (disk inode) 구조체가 나타낸다. XFS의 아이노드는 필요에 따라 크기가 증가될 수 있는 가변적인 구조이므로 핵심이 되는 xfs_dinode_core_t 구조체가 나타내는 헤더 정보를 앞에 포함하고, 필요에 따라 union으로 정의된 멤버들을 추가하도록 한다. xfs_dinode_core_t에 포함된 시간 정보(atime, mtime, ctime)들은 다음의 구조체에서 볼 수 있듯이 64비트로 관리된다.

typedef struct xfs_timestamp {
    __int32_t    t_sec;    /* timestamp seconds */
     __int32_t    t_nsec;    /* timestamp nanoseconds */
} xfs_timestamp_t;

디렉토리 구조
XFS의 디렉토리 구조는 버전 1, 2의 두 가지로 나뉜다. 버전 1에서는 디렉토리 내의 모든 정보가 아이노드 내에 포함될 만큼 작은 경우에는 별도의 블럭을 할당하지 않고, 아이노드 내에 디렉토리 정보를 유지하는 short form과 별도의 B+ 트리 블럭을 할당하여 정보를 저장하는 large form이 있다. 버전 2에서는 이를 확장하여 <표 1>과 같이 총 4가지 경우가 가능하다.

<표 > XFS의 디렉토리 구조(버전 2)

B+ 트리에서 사용되는 블럭들에 대해서는 이후에 좀 더 자세히 살펴보기로 하자.

XFS의 자료 구조
B 트리
B 트리는 본래 디스크와 같은 보조 기억 장치에서 사용되기 위해 연구된 균형 탐색 트리로 디스크의 I/O 연산을 최소화하도록 설계되었다. B 트리의 노드는 일반 이진 트리와는 달리 매우 많은 자식 노드를 가질 수 있다. 이를 ‘Branching factor’라고 하며 보통 수천 개의 자식 노드를 가지는데 이는 디스크의 블럭 크기에 따라 결정된다. 따라서 B 트리의 높이는 일반 이진 트리보다 훨씬 낮아지게 되므로 방문해야 할 노드의 수가 줄어들게 된다.

<그림 1> B 트리의 구조

<그림 1>에서 각 노드에 포함된 알파벳은 검색에 사용될 키를 나타낸다. 각각의 키는 그에 연관된 데이터를 가리키는 포인터 정보를 함께 유지하고 있다. 루트가 아닌 노드는 최소 t개에서 최대 2t개까지의 자식을 가질 수 있다. (t는 2 이상의 정수) 앞의 예제에서 t = 2가 되지만 보통 t의 값은 이보다 매우 큰 값이 된다.

B+ 트리
B+ 트리는 B 트리의 변형으로 기본적인 개념은 B 트리와 유사하지만 순차적인 접근의 효율성을 높이기 위해 모든 데이터를 리프 노드에 유지하고 이를 순차적으로 연결한 구조이다.

<그림 2> B+ 트리의 구조

B+ 트리에서 중간 노드에 있는 키 값은 리프 노드에 있는 키 값을 찾아갈 수 있는 경로로만 제공되므로 중간 노드에 있는 키 값은 모두 리프 노드에 다시 나타난다. 리프 노드들은 모두 링크드 리스트로 연결돼 있으므로 파일의 내용을 순서대로 읽는 경우와 같은 순차적 접근 처리에 효율적이다.

XFS의 B+ 트리 구조
XFS에서 사용되는 B+ 트리는 앞에서 살펴봤듯이 중간 노드인 경우 xfs_da_intnode_t 구조체로, 리프 노드인 경우 xfs_dir2_leaf_t 구조체로 정의된다.

<그림 3> XFS의 B+ 트리 노드 구조

중간 노드와 리프 노드 모두 거의 비슷한 헤더 형식을 사용한다. hdr 구조체의 forw와 back 멤버는 이중 링크드 리스트를 관리하기 위해 사용되며 magic 값은 각각 XFS_DA_NODE_MAGIC (0xfebe)와 XFS_DIR2_LEAFN_MAGIC (0xd2ff) 값이 사용된다. pad 멤버는 구조체의 정렬을 위해 들어간 의미 없는 변수이다.

먼저 중간 노드부터 살펴보면 count 멤버는 현재 노드에 포함된 전체 엔트리(btree)의 수이고, level은 리프 노드에서부터의 높이를 나타낸다. 각각의 엔트리는 xfs_da_node_entry 구조체로서 hashval과 before라는 멤버를 가지는데 hashval은 키로 사용되고 before 멤버는 해당 키보다 앞쪽에 존재하는 데이터(블럭 번호)를 저장한다. xfs_da_intnode_t 구조체에는 초기에 오직 하나의 btree 구조체 만이 존재하지만 새로운 블럭이 할당되는 경우 xfs_da_node_add() 함수에 의해 새로운 구조체가 할당되어 btree 배열에 추가된다.

리프 노드도 비슷한 방식으로 정보를 저장하는데 count에는 전체 엔트리의 수를, stale에는 제거할 수 있는 오래된 엔트리의 수를 저장한다. 각각의 엔트리는 xfs_dir2_leaf_entry_t 구조체로서 마찬가지로 hashval과 address라는 멤버를 가진다. hashval은 중간 노드에서처럼 키로 사용되고 address 멤버에는 실제 데이터가 저장된 블럭의 주소를 저장한다.

XFS 초기화
XFS의 초기화 루틴은 <fs/xfs/linux-2.6/xfs_super.c> 파일에 정의되어 있는 init_xfs_fs() 함수가 맡아 수행한다.

STATIC int __init
init_xfs_fs( void )
{
    int         error;
    struct sysinfo     si;
    static char     message[] __initdata = KERN_INFO \
        XFS_VERSION_STRING " with " XFS_BUILD_OPTIONS " enabled\n";

    printk(message);

    si_meminfo(&si);
    xfs_physmem = si.totalram;

    ktrace_init(64);

    error = init_inodecache();
    if (error < 0)
        goto undo_inodecache;

    error = pagebuf_init();
    if (error < 0)
        goto undo_pagebuf;

    vn_init();
    xfs_init();
    uuid_init();
    vfs_initquota();

    xfs_inode_shaker = kmem_shake_register(xfs_inode_shake);
    if (!xfs_inode_shaker) {
            error = -ENOMEM;
            goto undo_shaker;
    }

    error = xfs_ioctl32_init();
    if (error)
        goto undo_ioctl32;

    error = register_filesystem(&xfs_fs_type);
    if (error)
        goto undo_register;
    XFS_DM_INIT(&xfs_fs_type);
    return 0;

먼저 XFS가 시작됐다는 메시지를 출력한 뒤 시스템의 메모리 정보를 읽어와 XFS가 사용할 수 있는 메모리의 양을 결정한다. 그리고 디버깅 정보를 위한 ktrace 영역을 할당한 뒤 inode 캐시를 초기화한다. 그리고 pagebuf 데몬과 그에 관련된 다른 데몬들을 초기화한다. 이들 데몬의 역할에 대해서는 다음에 자세히 살펴보기로 한다. 그리고는 각각의 초기화 함수들을 호출한다.

먼저 vn_init() 함수는 <fs/xfs/linux/xfs_vnode.c>에 정의되어 있으며 vsync 배열에 속한 각 동기화 변수들을 init_waitqueue_head() 함수를 이용하여 초기화한다. 다음으로 xfs_init() 함수는 <fs/xfs/xfs_vfsops.c>에 정의되어 있으며 XFS에서 사용되는 핵심 자료 구조들(블럭 맵핑을 위한 프리 리스트, B+ 트리에서 사용되는 커서, 아이노드, 트랜잭션 구조체)을 초기화하는 역할을 한다. 그리고 UUID와 quota 관리에 필요한 초기화를 한 뒤 아이노드 캐시를 관리하기 위한 xfs_inode_shake를 등록한다. 그리고 64비트와 32비트 시스템 간의 ioctl 호환성을 위해 xfs_ioctl32_init() 함수를 수행한 뒤 파일 시스템을 등록하고 XFS_DM_INIT() 매크로를 호출하여 데이터 관리 연산에 필요한 초기화를 수행한다.

파일 시스템 마운트
XFS는 <fs/xfs/linux-2.6/xfs_super.c>에 다음과 같이 설정되어 있다.

STATIC struct file_system_type xfs_fs_type = {
        .owner                 = THIS_MODULE,
        .name                 = "xfs",
        .get_sb                 = linvfs_get_sb,
        .kill_sb                = kill_block_super,
        .fs_flags             = FS_REQUIRES_DEV,
};

마운트 과정에서 파일 시스템 이름으로 검색한 file_system_type 구조체의 get_sb 함수가 호출된다. linvfs_get_sb 함수는 linvfs_fill_super() 함수와 함께 get_sb_bdev() 함수를 호출하여 블럭 디바이스의 0번 블럭 데이터를 읽은 후에 linvfs_fill_super() 함수에 데이터를 넘겨 XFS의 슈퍼 블럭 구조체의 정보를 올바로 설정한다.

linvfs_fill_super() 함수는 bhv_insert_all_vfsops() 함수를 통해 가상 파일 시스템에서 지원하는 각종 연산들과 quota management와 data management에 필요한 연산들을 vfs 구조체에 설정한다. 그리고는 VFS_PARSEARG() 매크로를 호출하여 마운트시의 옵션들을 xfs_mount_args 구조체에 저장한 뒤 VFS_MOUNT() 매크로를 호출하여 실제 마운트 연산을 수행하는 xfs_mount() 함수를 호출한다. 그리고 d_alloc_root() 함수를 호출하여 루트 디렉토리 엔트리(“/”)를 할당하고 linvfs_start_syncd() 함수를 통해 xfssyncd 데몬을 생성하여 수행시킨다.

xfs_mount() 함수는 <fs/xfs/xfs_vfsops.c> 파일에 정의되어 있는데 XFS의 파티션은 데이터와 로그 정보가 하나의 논리 볼륨 내에 기록되는 경우, 데이터와 로그 정보가 별도의 서브 볼륨으로 기록되는 경우, 데이터와 로그 정보에 실시간 서브 볼륨이 추가되는 3가지 경우가 있을 수 있으므로 이를 모두 처리할 수 있도록 되어 있다.

STATIC int
xfs_mount(
        struct bhv_desc         *bhvp,
        struct xfs_mount_args *args,
        cred_t                 *credp)
{
        struct vfs             *vfsp = bhvtovfs(bhvp);
        struct bhv_desc         *p;
        struct xfs_mount        *mp = XFS_BHVTOM(bhvp);
        struct block_device     *ddev, *logdev, *rtdev;
        int                     flags = 0, error;

        ddev = vfsp->vfs_super->s_bdev;
        logdev = rtdev = NULL;

        /*
         * Setup xfs_mount function vectors from available behaviors
         */
        p = vfs_bhv_lookup(vfsp, VFS_POSITION_DM);
        mp->m_dm_ops = p ? *(xfs_dmops_t *) vfs_bhv_custom(p) : xfs_dmcore_stub;
        p = vfs_bhv_lookup(vfsp, VFS_POSITION_QM);
        mp->m_qm_ops = p ? *(xfs_qmops_t *) vfs_bhv_custom(p) : xfs_qmcore_stub;
        p = vfs_bhv_lookup(vfsp, VFS_POSITION_IO);
        mp->m_io_ops = p ? *(xfs_ioops_t *) vfs_bhv_custom(p) : xfs_iocore_xfs;

        /*
         * Open real time and log devices - order is important.
         */
        if (args->logname[0]) {
                error = xfs_blkdev_get(mp, args->logname, &logdev);
                if (error)
                        return error;
        }
        if (args->rtname[0]) {
                error = xfs_blkdev_get(mp, args->rtname, &rtdev);
                if (error) {
                        xfs_blkdev_put(logdev);
                        return error;
                }

                if (rtdev == ddev || rtdev == logdev) {
                        cmn_err(CE_WARN,
        "XFS: Cannot mount filesystem with identical rtdev and ddev/logdev.");
                        xfs_blkdev_put(logdev);
                        xfs_blkdev_put(rtdev);
                        return EINVAL;
                }
        }

데이터, 로그, 실시간 서브 볼륨 정보를 초기화한다. 데이터 서브 볼륨의 경우에는 get_sb_bdev() 함수에서 이미 읽어두었으므로 기억된 정보를 설정하고, 로그 서브 볼륨과 실시간 서브 볼륨의 경우에는 마운트 옵션으로 logdev 와 rtdev가 주어진 경우에만 설정하도록 한다.

다음으로 마운트 정보를 가지는 구조체에서 사용할 데이터 관리 연산(m_dm_ops)과 quota 관리 연산(m_qm_ops)과 IO 연산(m_io_ops) 정보를 설정한다. 마운트시 옵션으로 로그 서브 볼륨과 실시간 서브 볼륨의 이름이 주어진 경우에는 해당하는 장치에 대한 정보를 각각 logdev와 rtdev 변수에 저장한다. 실시간 서브 볼륨의 경우에는 데이터 서브 볼륨이나 로그 서브 볼륨과 같은 장치를 사용할 수 없다.

        error = ENOMEM;
        mp->m_ddev_targp = xfs_alloc_buftarg(ddev);
        if (!mp->m_ddev_targp) {
                xfs_blkdev_put(logdev);
                xfs_blkdev_put(rtdev);
                return error;
        }
        if (rtdev) {
                mp->m_rtdev_targp = xfs_alloc_buftarg(rtdev);
                if (!mp->m_rtdev_targp)
                        goto error0;
        }
        mp->m_logdev_targp = (logdev && logdev != ddev) ?
                                xfs_alloc_buftarg(logdev) : mp->m_ddev_targp;
        if (!mp->m_logdev_targp)
                goto error0;

그리고 각각의 서브 볼륨에 대한 버퍼 타겟 구조체를 할당한다. 로그 서브 볼륨은 데이터 서브 볼륨과 같이 사용될 수 있으므로 이 경우 동일한 버퍼 타겟을 이용하도록 한다.

        /*
         * Setup flags based on mount(2) options and then the superblock
         */
        error = xfs_start_flags(vfsp, args, mp);
        if (error)
                goto error1;
        error = xfs_readsb(mp);
        if (error)
                goto error1;
        error = xfs_finish_flags(vfsp, args, mp);
        if (error)
                goto error2;

마운트 시에 주어진 옵션들의 정보를 가지고 있는 args 구조체를 검사하여 마운트 구조체의 플래그 값을 설정한다. 그리고 xfs_readsb() 함수를 호출하여 슈퍼 블럭(0번 블럭)의 내용을 읽고 이를 슈퍼 블럭 구조체에 설정한 뒤 올바른 값으로 설정되었는지 검사하는 과정을 거쳐 마운트 구조체의 m_sb_nb 필드에 저장한다.

        error = xfs_setsize_buftarg(mp->m_ddev_targp, mp->m_sb.sb_blocksize,
                                    mp->m_sb.sb_sectsize);
        if (!error && logdev && logdev != ddev) {
                unsigned int    log_sector_size = BBSIZE;

                if (XFS_SB_VERSION_HASSECTOR(&mp->m_sb))
                        log_sector_size = mp->m_sb.sb_logsectsize;
                error = xfs_setsize_buftarg(mp->m_logdev_targp,
                                            mp->m_sb.sb_blocksize,
                                            log_sector_size);
        }
        if (!error && rtdev)
                error = xfs_setsize_buftarg(mp->m_rtdev_targp,
                                            mp->m_sb.sb_blocksize,
                                            mp->m_sb.sb_sectsize);
        if (error)
                goto error2;

        error = XFS_IOINIT(vfsp, args, flags);
        if (!error)
                return 0;

        …
}

각각의 버퍼 타겟의 크기를 설정한다. 데이터 서브 볼륨과 실시간 서브 볼륨의 경우에는 슈퍼 블럭 구조체에 저장되어 있는 sb_blocksize와 sb_sectsize 값을 이용하고 로그 서브 볼륨의 경우 별도의 sb_logsectsize 값이 있다면 이를 이용하고, 없다면 기본 블럭 크기인 BBSIZE(512바이트)를 사용하도록 한다. 그리고 XFS_IOINIT() 매크로를 호출하는데 이 매크로는 xfs_mountfs() 함수를 호출하도록 정의되어 있다.

xfs_mountfs() 함수는 읽어온 슈퍼 블럭 정보로 마운트 구조체 정보를 설정하고 32비트 커널인 경우 테라바이트 단위의 파일 시스템을 마운트하지 않도록 한다. 그리고 마운트 구조체의 실시간 서브 볼륨에 관련된 정보들을 설정한 뒤 아이노드 해시 테이블을 설정하고 디렉토리 관리를 위한 초기화 작업을 수행한다. 마지막으로 로그 매니저를 초기화한 후 필요한 경우 복구 루틴을 수행한다.

XFS 데몬
XFS는 기본적으로 4 종류의 데몬을 이용한다. 이 중 xfssyncd와 xfsbufd 데몬은 CPU의 수에 관계없이 하나씩만 존재하고, xfsdatad와 xfslogd 데몬은 SMP 시스템의 경우 각 CPU마다 하나씩 존재한다.

◆ xfssyncd : 마운트 과정의 linvfs_fill_super() 함수 내에서 linvfs_start_syncd() 함수에 의해 커널 쓰레드로 생성된다. 실제 수행하는 루틴은 <fs/xfs/linux-2.6/xfs_super.c>에 정의되어 있는 xfssyncd() 함수이다. 이 함수는 무한 루프를 돌며 xfs_syncd_centisecs 주기마다 vfs_sync_worker() 함수에 의해 xfs_syncsub() 함수를 호출하여 로그 정보와 메타 데이터 정보들을 기록한다.

◆ xfsbufd : 파일 시스템 초기화 과정의 xfs_fs_init() 함수 내에서 pagebuf_daemon_start() 함수에 의해 커널 쓰레드로 생성된다. 실제 수행하는 루틴은 <fs/xfs/linux-2.6/xfs_buf.c>에 정의되어 있는 pagebuf_daemon() 함수이다. 이 함수는 무한 루프를 돌며 xfs_buf_timer_centisecs 주기마다 pdb_delwrite_queue 에 들어있는 지연된 쓰기 연산에 대해 pagebuf_iostrategy() 함수와 blk_run_address_space() 함수를 호출하여 I/O 요청을 처리한다.

◆ xfsdatad/[cpu], xfslogd/[cpu] : 파일 시스템 초기화 과정의 xfs_fs_init() 함수 내에서 pagebuf_daemon_start() 함수에 의해 커널 쓰레드로 생성된다. 실제 수행하는 루틴은 <kernel/workqueue.c> 내의 worker_thread() 함수이다. 이 함수는 set_user_nice() 함수를 이용하여 자신의 우선순위를 높인 뒤 무한 루프를 돌며 주어진 워크 큐에 수행할 작업이 들어 있는 경우 run_workqueue() 함수를 호출하여 이를 수행한다. xfsdatad와 xfslogd의 경우에는 각각 pagebuf_dataio_workqueue 와 pagebuf_logio_workqueue를 관리한다.

XFS의 파일 연산
데이터 연산
리눅스의 가상 파일 시스템의 파일 연산 구조체는 관련된 아이노드에서 관리한다. XFS에서 이러한 파일 연산 구조체를 설정하는 과정은 vnode 에 대한 초기화 함수가 수행되는 과정에서 호출되는 xfs_set_inodeops() 함수에서 이뤄진다. 이 함수는 <fs/xfs/linux-2.6/xfs_super.c> 파일에 정의되어 있다.

STATIC void
xfs_set_inodeops(
        struct inode            *inode)
{
        vnode_t                 *vp = LINVFS_GET_VP(inode);

        if (vp->v_type == VNON) {
                vn_mark_bad(vp);
        } else if (S_ISREG(inode->i_mode)) {
                inode->i_op = &linvfs_file_inode_operations;
                inode->i_fop = &linvfs_file_operations;
                inode->i_mapping->a_ops = &linvfs_aops;
        } else if (S_ISDIR(inode->i_mode)) {
                inode->i_op = &linvfs_dir_inode_operations;
                inode->i_fop = &linvfs_dir_operations;
        } else if (S_ISLNK(inode->i_mode)) {
                inode->i_op = &linvfs_symlink_inode_operations;
                if (inode->i_blocks)
                        inode->i_mapping->a_ops = &linvfs_aops;
        } else {
                inode->i_op = &linvfs_file_inode_operations;
                init_special_inode(inode, inode->i_mode, inode->i_rdev);
        }
}

이 함수는 주어진 아이노드의 타입을 검사한 뒤 그에 따라 적절한 파일 연산 구조체를 설정한다. 일반 파일의 경우(S_ISREG) 파일 연산 구조체는 linvfs_file_operations가 된다. 이 구조체는 <fs/xfs/linux-2.6/xfs_file.c>에 다음과 같이 정의되어 있다.

struct file_operations linvfs_file_operations = {
        .llseek         = generic_file_llseek,
        .read         = do_sync_read,
        .write         = do_sync_write,
        .readv         = linvfs_readv,
        .writev         = linvfs_writev,
        .aio_read     = linvfs_read,
        .aio_write     = linvfs_write,
        .sendfile     = linvfs_sendfile,
        .ioctl         = linvfs_ioctl,
        .mmap         = linvfs_file_mmap,
        .open         = linvfs_open,
        .release        = linvfs_release,
        .fsync         = linvfs_fsync,
};

읽기 연산에 대해서 살펴보면 do_sync_read() 함수가 사용되는 데 이 함수는 동기화에 관련된 정보들을 적절히 설정한 뒤 file_operations 구조체의 aio_read 멤버가 가리키는 함수를 호출한다. linvfs_read() 함수는 ioflags 인자를 0으로 설정하여 __linvfs_read() 함수를 호출한다. 이 함수는 <fs/xfs/linux-2.6/xfs_file.c>에 다음과 같이 정의되어 있다.

STATIC inline ssize_t
__linvfs_read(
        struct kiocb            *iocb,
        char                    __user *buf,
        int                     ioflags,
        size_t                 count,
        loff_t                 pos)
{
        struct iovec            iov = {buf, count};
        struct file             *file = iocb->ki_filp;
        vnode_t                 *vp = LINVFS_GET_VP(file->f_dentry->d_inode);
        ssize_t                 rval;

        BUG_ON(iocb->ki_pos != pos);

        if (unlikely(file->f_flags & O_DIRECT))
                ioflags |= IO_ISDIRECT;
        VOP_READ(vp, iocb, &iov, 1, &iocb->ki_pos, ioflags, NULL, rval);
        return rval;
}

이 함수는 주어진 파일에 O_DIRECT 플래그가 설정된 경우에 ioflags 플래그에 IO_ISDIRECT 플래그를 더하여 VOP_READ() 매크로가 가리키는 read 함수를 호출한다. 이 과정에서 파일의 아이노드로부터 vnode_t의 정보를 추출하여 여기에 저장된 vnodeops_t 구조체의 read 멤버가 가리키는 xfs_read() 함수가 호출된다. xfs_read() 함수는 <fs/xfs/linux-2.6/xfs_lrw.c> 파일에 정의되어 있다.

ssize_t                 /* bytes read, or (-) error */
xfs_read(
        bhv_desc_t             *bdp,
        struct kiocb            *iocb,
        const struct iovec     *iovp,
        unsigned int            segs,
        loff_t                 *offset,
        int                     ioflags,
        cred_t                 *credp)
{
        struct file             *file = iocb->ki_filp;
        struct inode            *inode = file->f_mapping->host;
        size_t                 size = 0;
        ssize_t                 ret;
        xfs_fsize_t             n;
        xfs_inode_t             *ip;
        xfs_mount_t             *mp;
        vnode_t                 *vp;
        unsigned long         seg;

        ip = XFS_BHVTOI(bdp);
        vp = BHV_TO_VNODE(bdp);
        mp = ip->i_mount;

        XFS_STATS_INC(xs_read_calls);

xfs_read() 함수는 우선 필요한 구조체에 대한 포인터를 설정하고, 관련 변수들을 선언한 뒤 XFS_STATS_INC() 매크로를 이용하여 읽기 연산이 요청되었음을 기록한다.

        for (seg = 0; seg < segs; seg++) {
                const struct iovec *iv = &iovp[seg];

                size += iv->iov_len;
                if (unlikely((ssize_t)(size|iv->iov_len) < 0))
                        return XFS_ERROR(-EINVAL);
        }

        if (unlikely(ioflags & IO_ISDIRECT)) {
                xfs_buftarg_t *target =
                        (ip->i_d.di_flags & XFS_DIFLAG_REALTIME) ?
                                mp->m_rtdev_targp : mp->m_ddev_targp;
                if ((*offset & target->pbr_smask) ||
                    (size & target->pbr_smask)) {
                        if (*offset == ip->i_d.di_size) {
                                return (0);
                        }
                        return -XFS_ERROR(EINVAL);
                }
        }

그리고 readv 함수에 의해 호출된 경우까지 고려하여 요청된 구간의 크기가 음수인지를 검사하고 이 경우 에러를 리턴한다. 그리고 ioflags에 IO_ISDIRECT가 설정된 경우라면 페이지 캐시를 거치지 않고 직접 읽기 연산이 수행되는 경우이므로 주어진 옵셋과 크기가 해당 디스크의 섹터 단위로 정렬되어 있는지 검사하여 에러를 리턴한다.

정렬되어 있지 않지만 옵셋이 파일의 크기와 같은 경우에는 파일의 끝에 도달한 것이므로 더 이상 읽을 데이터가 없기 때문에 바로 0을 리턴한다.

        n = XFS_MAXIOFFSET(mp) - *offset;
        if ((n <= 0) || (size == 0))
                return 0;

        if (n < size)
                size = n;

        if (XFS_FORCED_SHUTDOWN(mp)) {
                return -EIO;
        }

        if (unlikely(ioflags & IO_ISDIRECT))
                down(&inode->i_sem);
        xfs_ilock(ip, XFS_IOLOCK_SHARED);

그리고 옵셋과 읽기 연산을 수행할 크기를 검사하여 적절히 설정한다. 만약 이전에 파일 시스템이 강제로 종료된 적이 있었다면 파일 시스템 내에 올바르지 못한 데이터가 있을 수 있으므로 읽기 연산은 바로 에러를 리턴한다. 그렇지 않다면 아이노드에 대한 공유 락을 획득한다.

        xfs_rw_enter_trace(XFS_READ_ENTER, &ip->i_iocore,
                                (void *)iovp, segs, *offset, ioflags);
        ret = __generic_file_aio_read(iocb, iovp, segs, offset);
        if (ret == -EIOCBQUEUED)
                ret = wait_on_sync_kiocb(iocb);
        if (ret > 0)
                XFS_STATS_ADD(xs_read_bytes, ret);

        xfs_iunlock(ip, XFS_IOLOCK_SHARED);

        if (likely(!(ioflags & IO_INVIS)))
                xfs_ichgtime(ip, XFS_ICHGTIME_ACC);

unlock_isem:
        if (unlikely(ioflags & IO_ISDIRECT))
                up(&inode->i_sem);
        return ret;
}

읽기 연산에 대한 트레이스 정보를 버퍼에 추가한 뒤 __generic_file_aio_read() 함수를 통해 실제 읽기 연산을 수행한다. __generic_file_aio_read() 함수는 파일 구조체에 O_DIRECT 플래그가 설정되었는지 여부에 따라 내부적으로 각각 generic_file_direct_IO() 함수와 do_generic_file_read() 함수를 호출한다. 그리고 읽은 바이트 수를 xs_read_bytes 변수에 추가하여 기록한 뒤 공유 락을 해제하고 아이노드의 접근 시간을 갱신한다. 만약 ioflags에 IO_INVIS 플래그가 설정되어 있는 경우라면 접근 시간을 갱신하지 않는다.

쓰기 연산의 경우도 대체로 읽기 연산과 비슷한 순서로 진행된다. vnodeops_t 구조체의 write 포인터는 xfs_write()를 가리킨다. 이 함수도 <fs/xfs/linux-2.6/xfs_lrw.c>에 정의되어 있다. 간략히 설명하자면 먼저 xfs_read()와 마찬가지로 쓰기 연산이 호출되었음을 기록한 후에 writev 호출에 의한 각각의 iovec의 세그먼트에 대해 쓰기 연산의 크기와 범위를 검사하여 올바른 인자만을 가려낸다. 그리고 파일 포인터와 쓰기 연산을 수행할 크기 값을 조정한 뒤 이전에 파일 시스템이 강제로 종료되었는지를 검사하여 이 경우 에러를 리턴하도록 한다.

또한 IO_ISDIRECT 플래그가 설정된 경우에 요청된 옵셋과 크기가 섹터 단위로 정렬되었는지 검사하고 아이노드에 대한 세마포어 연산이나 페이지 캐시에 대한 flush 연산이 필요한지를 체크한다. 그리고 아이노드에 대한 배타적 락을 획득한 뒤 generic_write_checks() 함수를 호출하여 쓰기 연산이 올바른지를 검사하고(IO_INVIS 플래그가 설정되지 않은 경우) 파일 접근 시간을 갱신한다.

ssize_t                         /* bytes written, or (-) error */
xfs_write(
        bhv_desc_t             *bdp,
        struct kiocb            *iocb,
        const struct iovec     *iovp,
        unsigned int            nsegs,
        loff_t                 *offset,
        int                     ioflags,
        cred_t                 *credp)
{
        …

        if (pos > isize) {
                error = xfs_zero_eof(BHV_TO_VNODE(bdp), io, pos,
                                        isize, pos + count);
                if (error) {
                        xfs_iunlock(xip, XFS_ILOCK_EXCL|iolock);
                        goto out_unlock_isem;
                }
        }
        xfs_iunlock(xip, XFS_ILOCK_EXCL);

        if (((xip->i_d.di_mode & S_ISUID) ||
            ((xip->i_d.di_mode & (S_ISGID | S_IXGRP)) ==
                (S_ISGID | S_IXGRP))) &&
             !capable(CAP_FSETID)) {
                error = xfs_write_clear_setuid(xip);
                if (likely(!error))
                        error = -remove_suid(file->f_dentry);
                if (unlikely(error)) {
                        xfs_iunlock(xip, iolock);
                        goto out_unlock_isem;
                }
        }

그리고 쓰기 연산이 현재 파일의 크기를 변경하는 경우에는 새로 할당되는 영역에 대해 xfs_zero_eof() 함수를 호출하여 0으로 초기화한다. 그리고 배타적 락을 해제한 뒤 루트로 실행되는 경우가 아니라면 파일의 setuid 비트를 제거하여 실행 파일의 setuid, setgid 비트를 변경하지 못하도록 방지한다.

retry:
        /* We can write back this queue in page reclaim */
        current->backing_dev_info = mapping->backing_dev_info;

        if ((ioflags & IO_ISDIRECT)) {
                if (need_flush) {
                        VOP_FLUSHINVAL_PAGES(vp, ctooff(offtoct(pos)),
                                        -1, FI_REMAPF_LOCKED);
                }

                if (need_isem) {
                        up(&inode->i_sem);
                        need_isem = 0;
                }

                xfs_rw_enter_trace(XFS_DIOWR_ENTER, io, (void *)iovp, segs,
                                *offset, ioflags);
                ret = generic_file_direct_write(iocb, iovp,
                                &segs, pos, offset, count, ocount);
        } else {
                xfs_rw_enter_trace(XFS_WRITE_ENTER, io, (void *)iovp, segs,
                                *offset, ioflags);
                ret = generic_file_buffered_write(iocb, iovp, segs,
                                pos, offset, count, ret);
        }

        current->backing_dev_info = NULL;

ioflags에 IO_ISDIRECT 플래그가 설정된 경우라면 need_flush, need_isem 값에 따라 적절한 처리를 한 뒤, generic_file_direct_write() 함수를 호출하여 직접 쓰기 연산을 수행한다. 그렇지 않다면 generic_file_buffered_write() 함수를 통해 페이지 캐시를 이용한 쓰기 연산을 수행한다. 그리고 파일의 크기가 변경된 경우 아이노드에 있는 파일 크기 값을 변경하고 쓴 바이트 수를 xs_write_bytes 변수에 추가하여 기록한다.

        if ((file->f_flags & O_SYNC) || IS_SYNC(inode)) {
                if (!(mp->m_flags & XFS_MOUNT_OSYNCISOSYNC) &&
                    !(xip->i_update_size)) {
                        xfs_inode_log_item_t    *iip = xip->i_itemp;

                        if (iip && iip->ili_last_lsn) {
                                xfs_log_force(mp, iip->ili_last_lsn,
                                                XFS_LOG_FORCE | XFS_LOG_SYNC);
                        } else if (xfs_ipincount(xip) > 0) {
                                xfs_log_force(mp, (xfs_lsn_t)0,
                                                XFS_LOG_FORCE | XFS_LOG_SYNC);
                        }

                } else {
                        xfs_trans_t     *tp;

                        tp = xfs_trans_alloc(mp, XFS_TRANS_WRITE_SYNC);
                        if ((error = xfs_trans_reserve(tp, 0,
                                                     XFS_SWRITE_LOG_RES(mp),
                                                     0, 0, 0))) {
                                /* Transaction reserve failed */
                                xfs_trans_cancel(tp, 0);
                        } else {
                                /* Transaction reserve successful */
                                xfs_ilock(xip, XFS_ILOCK_EXCL);
                                xfs_trans_ijoin(tp, xip, XFS_ILOCK_EXCL);
                                xfs_trans_ihold(tp, xip);
                                xfs_trans_log_inode(tp, xip, XFS_ILOG_CORE);
                                xfs_trans_set_sync(tp);
                                error = xfs_trans_commit(tp, 0, NULL);
                                xfs_iunlock(xip, XFS_ILOCK_EXCL);
                                if (error)
                                        goto out_unlock_internal;
                        }
                }

                xfs_rwunlock(bdp, locktype);
                if (need_isem)
                        up(&inode->i_sem);

                error = sync_page_range(inode, mapping, pos, ret);
                if (!error)
                        error = ret;
                return error;
        }

out_unlock_internal:
        xfs_rwunlock(bdp, locktype);
out_unlock_isem:
        if (need_isem)
                up(&inode->i_sem);
        return -error;
}

파일이나 아이노드에 O_SYNC 플래그가 설정되어 있다면 로그를 같이 기록한다. 마운트 시에 osyncisosync 옵션이 주어지지 않았고 파일의 크기가 변하지 않았다면 xfs_log_force() 함수를 이용하여 아이노드에 대한 로그를 모두 기록한다.

파일 크기가 변한 경우에는 트랜잭션 구조체를 생성하여 아이노드가 변경되었음을 등록하여 시간 정보를 갱신하도록 로그를 기록한다. 그리고 sync_page_range() 함수를 이용하여 페이지 캐시의 데이터를 디스크에 기록한다.

트랜잭션 관리
트랜잭션은 로그 관리 코드에 의해 제공되는 기능을 이용하여 깔끔하고 효율적인 저널링 메카니즘을 제공한다. XFS의 트랜잭션 관리는 디스크 캐시, 아이노드 관리, 로그 관리 인터페이스의 상위 계층에서 이루어지며 파일 시스템의 메타 데이터는 물론 사용자 데이터에도 적용된다. XFS의 트랜잭션 관리가 진행되는 순서는 다음과 같다.

[1] alloc - 구조체와 트랜잭션 ID 생성 : 트랜잭션 구조체의 생성은 xfs_trans_alloc() 함수가 담당하는데 이 함수는 파일 시스템이 freeze 되어 있는지 검사한 후에 트랜잭션의 카운트 값을 증가시키고 _xfs_trans_alloc() 함수를 호출한다. 이 함수는 xfs_trans_zone 구조체를 할당받아 적절히 초기화한다.

[2] reserve - 로그 공간 확보(데드락 방지) : xfs_trans_reserve() 함수는 로그를 위한 공간으로 인자로 주어진 logspace * logcount 만큼의 공간을 확보한다. 트랜잭션이 진행되는 동안 로그 연산에 필요한 공간이 부족하게 되면 데드락에 빠질 수 있으므로 항상 모든 로그 연산이 수행될 수 있는 충분한 공간이 확보되어야 한다.

[3] lock - 자원에 대한 락 획득 : two-phase locking의 개념을 도입하여 관련된 자원에 대한 락을 획득한다. 획득한 락은 모든 연산이 끝나고 트랜잭션이 커밋되기 전에 해제되어서는 안 된다.

[4] modify - 데이터 변경 : 실제로 작업하고자 하는 자원의 데이터를 변경한다. 변경 사항은 이후에 로그로 기록하기 위해 기억해야 한다. 이 단계에서 오퍼레이션 로깅을 위한 트랜잭션 오퍼레이션 구조체를 생성할 수 있다.

[5] commit - 로그 정보를 메모리에 기록 : xfs_trans_commit() 함수는 변경 사항과 트랜잭션 오퍼레이션을 in-core log(메모리)에 기록한다. 이 단계에서 변경된 자원은 pin되었다고 하며 해당 자원에 대한 락을 해제한다. 현재 트랜잭션의 정보는 아직 디스크에 기록되지 않은 상태이므로 영구적이지 않다.

[6] log - 로그 정보를 디스크에 기록 : 메모리 내의 in-core log 정보를 디스크 상에 영구적으로 기록하는 과정이다. 이것은 트랜잭션을 수행하는 루틴 내에서 직접 처리하거나 혹은 xfs_trans_set_sync() 함수를 이용하여 XFS_TRANS_SYNC 플래그를 설정하고 xfs_trans_commit() 함수의 호출 과정에서 간접적으로 처리하도록 할 수 있다. 이 과정이 끝나면 트랜잭션 구조체 정보가 해제되고 자원은 unpinned 상태가 된다. 트랜잭션 내에서 변경된 데이터나 오퍼레이션 로깅 정보들을 AIL(Active Item List)에 저장한다. 이 리스트는 트랜잭션이나 로깅 과정에서 로그 내의 유효한 데이터의 위치를 찾기 위해 사용된다.

[7] flush - 데이터 변경 사항을 디스크에 기록 : 로그 정보가 디스크에 기록되고 자원이 unpinned 되었으므로 해당 자원의 변경 사항을 디스크에 기록한다. 변경된 자원이 디스크에 기록되면 자동으로 AIL에서 제거된다. 이후에 해당 자원은 새로운 트랜잭션을 위해 재사용될 수 있다.

로그 관리
로그 관리 서비스는 파일 시스템이 손상되고 난 후에 신속하고 신뢰성 있는 복구 메카니즘을 제공하기 위해 사용되며 파일 시스템 메타 데이터를 변경하는 연산에 대해 더 높은 성능을 제공한다. XFS는 로그 정보를 기록하기 위한 별도의 서브 볼륨을 사용할 수 있으며 이는 마운트 구조체의 m_logdev 필드에서 유지한다. 고수준의 관점에서 디스크 상의 로그 정보는 원형 큐의 형태이다.

트랜잭션이 수행될 때 트랜잭션을 수행하는 클라이언트는 트랜잭션 관리자에게 트랜잭션 아이디를 요청한다. 트랜잭션 아이디는 UUID(Universal Unique IDentifier)이며, 트랜잭션 관리자는 클라이언트에게 트랜잭션 아이디를 리턴하기 전에 로그 관리자에게 트랜잭션이 시작되었음을 알리는 로그 정보를 보낸다. 클라이언트는 연산에 관련된 로그 정보를 TID와 함께 로그 관리자에게 직접 보낸다.

트랜잭션이 완료되면 클라이언트는 로그 관리자를 호출하여 트랜잭션이 종료되었음을 알린다. 트랜잭션의 형태에 따라 commit 연산은 트랜잭션에 관련된 메타 데이터 정보들이 디스크에 기록될 때까지 블럭되거나 콜백 메카니즘을 이용하여 비동기적으로 처리한다.

시스템이 손상되면 로그 관리자는 디스크 상의 로그 정보를 읽어 맨 마지막 로그 정보와 손상되지 않은 마지막 정보를 조사한 뒤 이후의 로그를 읽어서 손상된 부분을 재빨리 복구한다. 이 과정은 <fs/xfs/xfs_log_recover.c>에 있는 xlog_recover() 함수에 의해 수행되는데 먼저 xlog_find_tail() 함수를 호출하여 디스크와 동기화 된 마지막 로그 레코드의 위치를 알아낸다(<그림 4>의 과정 0).

이 정보를 이용하여 실제 복구 연산을 수행할 xlog_do_log_recovery() 함수를 호출하는데 이 과정은 동기화되지 않은 로그 정보를 두 번 탐색하여 수행된다. 첫 번째 탐색 과정에서는 XLOG_RECOVER_PASS1 인자와 함께 xlog_do_recovery_pass() 함수를 호출하여 취소된 로그 아이템 정보를 읽어(<그림 4>의 과정 1) 이를 xfs_buf_cancel_t 구조체에 저장한다. 두 번째 탐색 과정에서는 XLOG_RECOVER_PASS2 인자와 함께 xlog_do_recovery_pass() 함수가 호출되어 xfs_buf_cancel_t 구조체에 저장된 아이템들을 이용하여 데이터를 복구한다(<그림 4>의 과정 2).

<그림 4> 로그 정보를 이용한 시스템 복구 과정

XFS의 확장 기능
64비트 주소 공간 지원
XFS는 커널 레벨에서 지원하는 64비트 호환 인터페이스를 사용하므로 32비트 응용 프로그램에서도 투명하게 64 비트 파일에 접근할 수 있도록 한다. 이를 위해 <fs/xfs/xfs_typs.h> 파일에는 시스템의 기본 크기에 따라 사용할 수 있는 각종 타입들을 정의한다.

블럭 디바이스 레이어에서 64비트 주소를 지원할 때 (CONFIG_LBD) XFS 볼륨의 최대 크기(최대 파일 크기)는 900만 테라바이트까지 지원된다. 32비트 플랫폼에서 2.6 커널을 사용하는 경우 블럭 디바이스 레이어에서 64비트 주소를 지원한다고 해도 파일 시스템의 최대 크기와 파일의 최대 크기는 16테라바이트로 제한된다.

DMAPI - HSM 지원
HSM(Hierachical Storage Management)은 값싸고 느린 저장 장치와 비싸고 빠른 저장 장치들을 계층적으로 관리하여 주로 사용하는 데이터 들을 보다 빠른 저장 장치에 저장함으로써 비용과 성능 상의 이점을 꾀하고자 하는 개념이다. 이상적으로 모든 저장 장치가 빠른 접근 속도를 내는 것이 가장 좋지만 그렇게 하기 위해서는 막대한 비용이 소요되므로 현실적인 이유로 느리지만 값싼 저장 장치를 많이 사용하는 데에서 비롯된 것이다. 간단히, 메모리 관리에서처럼 저장 장치의 관리에 있어서도 캐시를 도입한 것이라고 생각할 수 있다.

XFS는 DMAPI를 통해 커널의 수정 없이 HSM 소프트웨어를 구현할 수 있도록 한다. DMAPI(혹은 XDSM)는 X/Open 문서인 <Systems Management: Data Storage Management (XDSM) API>의 구현이다. XFS에서 libdm 라이브러리를 통해 DMAPI 인터페이스를 제공하는 함수를 등록할 수 있으며 SGI의 DMF, Veritas의 Veritas HSM 등이 이를 지원한다.

실시간 서브 볼륨
XFS는 실시간 데이터를 저장하기 위한 실시간 서브 볼륨을 옵션으로 제공한다. 실시간 서브 볼륨은 일반적인 B+ 트리로 구현된 할당자가 아닌 비트맵을 이용한 실시간 할당자가 사용된다. 이러한 실시간 서브 볼륨은 멀티미디어 스트리밍 응용 프로그램과 같이 신뢰할 만한 데이터 전송률을 보장해야 하는 영역에서 유용하게 사용될 수 있다.

실시간 서브 볼륨의 공간 할당은 <fs/xfs/xfs_rtalloc.c> 에 정의된 xfs_rtallocate_extent() 함수에서 수행되며 주어진 할당 타입에 따라 각각 xfs_rtallocate_extent_size(), xfs_rtallocate_extent_near, xfs_rtallocate_extent_exact() 함수를 호출한다. XFS에서 할당 시 요청할 수 있는 할당 타입을 <표 2>에 나타내었다. 실시간 서브 볼륨에서는 이 중 3가지만이 사용된다.

<표 2> XFS의 할당 타입

디스크 공간 할당 시에는 요청하는 실제 크기 정보와 함께 최대 크기와 최소 크기 정보가 제공되어 우선 최대 크기만큼 할당이 가능한지 검사하여 불가능한 경우에 최소 크기 이상의 공간을 할당하도록 한다.

다른 파일 시스템 분석에 도움이 되길
이번 글에서는 고성능 저널링 파일 시스템인 XFS에 대해서 간략히 알아보았다. 제한된 지면과 시간으로 인해 XFS을 낱낱이 파헤쳐 보지 못한 아쉬움이 남지만 어느 정도 윤곽을 잡을 수 있었기를 바라며 앞으로도 다른 파일 시스템을 분석할 때 도움이 될 만한 자료가 되기를 바란다. 다음 연재에서는 마지막으로 리눅스의 최대 장점 중의 하나로 꼽히는 네트워크 서브 시스템에 대해서 살펴보기로 한다.@

저널링 파일 시스템 XFS - 출처 ZDNet

저널링 파일 시스템 XFS - 출처 ZDNet”에 대한 2개의 생각

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다