코드가 3,000 라인을 넘어가는 관계로 코드 분석을 파트별로 나누어 진행하려 한다. main
함수부터 순서대로 따라가되 맥락 별로 하부 내용을 생략하려 한다. 코드 전문은 필자의 github
에서 확인하길 바란다.
그리고 책에 삽입되어 있는 코드 자체가 매우 buggy
하기 때문에 필자가 작성한 코드를 그대로 쓰는 것을 추천한다. 위 책의 삽입된 코드를 필자가 직접 다 옮겨 작성하고 많은 버그를 고친 버전이다.
main.c
main()
#include <stdio.h>
#include <stdlib.h>
#include "shell.h"
int main(int argc, char *argv[])
{
struct shell *shell;
shell = shell_create();
while (shell_run(shell)) ;
shell_destroy(shell);
return 0;
}
main
에서 하는 일은 별거 없다. shell
객체의 생성, 실행, 제거를 수행한다.
shell.c
shell_create()
struct shell *shell_create(void)
{
struct shell *ret;
ret = malloc(sizeof(struct shell));
if ( !ret )
return NULL;
ret->commands = shell_cmd_list_get_list();
ret->command_count = shell_cmd_list_get_size();
ret->is_mounted = false;
ret->path_top = 0;
shell_register_filesystem(&ret->filesystem);
if (disksim_init(NUMBER_OF_SECTORS, SECTOR_SIZE, &ret->disk) < 0) {
free(ret);
return NULL;
}
return ret;
}
shell
객체 생성 및 명령어 등록을 수행한다.shell_register_filesystem()
를 호출해 filesystem
명령어(mount
, umount
, format
) 를 등록한다.disksim_init()
으로 디스크 정보를 초기화한다.함수 자체의 설명은 이후에 이어진다.
struct shell
은 다음의 멤버를 가진다:
struct shell {
struct shell_command *commands;
int command_count;
bool is_mounted;
struct disk_operations disk;
struct shell_filesystem filesystem;
struct shell_fs_operations fops;
struct shell_entry rootdir, curdir;
struct shell_entry path[256];
int path_top;
};
shell_create()
통해 초기화되는 객체는 크게 commands
, disk
, filesystem
이다. 그 외의 객체들은 이후 format
, mount
명령어 등으로 초기화가 이뤄진다.
shell_run()
int shell_run(struct shell *shell)
{
char buffer[BUFSIZ];
char *argv[BUFSIZ];
int argc;
printf("%s file system shell\n", shell->filesystem.name);
while (true) {
if (shell->path_top == 0)
fputs("[/] # ", stdout);
else
printf("[/%s] # ", shell->curdir.name);
fgets(buffer, BUFSIZ - 1, stdin);
argc = seperate_string(buffer, argv);
if (argc == 0)
continue;
int i, retval;
for (i = 0; i < shell->command_count; i++) {
if (strcmp(shell->commands[i].name, argv[0]) == 0) {
if (check_conditions(shell, i)) {
retval = shell->commands[i].handle(
shell, argc, argv
);
if (retval == -256)
return 0;
} else {
puts("this command is currently "
"unavailable!");
}
break;
}
}
if (shell->command_count == i) {
puts("unknown command!");
shell_show_commands(shell);
}
}
return -1;
}
shell_run()
은 현재 디렉터리 이름을 prompt
하고 사용자의 명령어 입력을 기다린다. 명령어를 입력하면 아래의 process
를 진행한다:
strcmp()
: 입력한 이름의 command
가 존재하는지 확인.check_conditions()
: 존재한다면 현 상황에서 수행 가능한 명령어인지 확인.handle()
: 명령어를 실행한다.shell_destroy()
void shell_destroy(struct shell *shell)
{
disksim_uninit(shell->disk);
free(shell);
}
shell
객체를 제거한다.
shell_command.c
static variables
static struct shell_command shell_command_list[] = {
{ "cd", shell_cmd_cd, CMD_COND_MOUNT },
{ "exit", shell_cmd_exit, CMD_COND_NONE },
{ "quit", shell_cmd_exit, CMD_COND_NONE },
{ "mount", shell_cmd_mount, CMD_COND_UMOUNT },
{ "umount", shell_cmd_umount, CMD_COND_MOUNT },
{ "touch", shell_cmd_touch, CMD_COND_MOUNT },
{ "fill", shell_cmd_fill, CMD_COND_MOUNT },
{ "rm", shell_cmd_rm, CMD_COND_MOUNT },
{ "ls", shell_cmd_ls, CMD_COND_MOUNT },
{ "dir", shell_cmd_ls, CMD_COND_MOUNT },
{ "format", shell_cmd_format, CMD_COND_UMOUNT },
{ "df", shell_cmd_df, CMD_COND_MOUNT },
{ "mkdir", shell_cmd_mkdir, CMD_COND_MOUNT },
{ "rmdir", shell_cmd_rmdir, CMD_COND_MOUNT },
{ "cat", shell_cmd_cat, CMD_COND_MOUNT }
};
static
으로 선언된 명령어 리스트이다. shell_command
객체는 아래의 형태를 가진다:
enum shell_command_condition {
CMD_COND_NONE = 0x00,
CMD_COND_MOUNT = 0x01,
CMD_COND_UMOUNT = 0x02
};
struct shell_command {
char *name;
int (*handle)(struct shell *, int, char **);
enum shell_command_condition conditions;
};
이후 각각의 명령어 설명은 명령어의 condition
에 따라 분류하여 설명한다.
shell_command.c
, (CMD_COND_NONE
)CMD_COND_NONE
조건을 가지는 명령어는 그 어떠한 상황에서도 수행 가능하다. 무-조건 실행 명령어라고 이해하면 된다.
shell_cmd_exit()
int shell_cmd_exit(struct shell *shell, int argc, char *argv[])
{
return -256;
}
바로 -256
을 반환한다. 이는 shell
의 종료를 요청하는 값으로써 해당 값이 반환되면 prompt
를 종료하고 shell_run()
함수를 반환한다.
shell_command.c
, (CMD_COND_UMOUNT
)CMD_COND_UMOUNT
명령어는 filesystem
이 mount
되지 않은 상태에서만 수행 가능한 명령어이다. 프로그램 실행 초기에는 CMD_COND_NONE
와 CMD_COND_UMOUNT
계열의 명령어만 수행 가능하다:
filesystem
을 초기화(format
)하고...mount
) 하는 과정을 거쳐...CMD_COND_MOUNT
계열 명령어 수행이 가능해진다.shell_cmd_format()
int shell_cmd_format(struct shell *shell, int argc, char *argv[])
{
int result;
char *param = NULL;
if (argc >= 2)
param = argv[1];
result = shell->filesystem.format(&shell->disk, param);
if (result < 0) {
printf("%s formatting is failed\n", shell->filesystem.name);
return -1;
}
printf("disk has been formatted successfully\n");
return 0;
}
shell_cmd_format()
명령어는 첫 번째 인자 (argv[1]
) 로 전달한 값에 해당하는 파일 시스템으로 filesystem
객체를 초기화한다. 현재 필자가 작성한 소스는 FAT12
만 지원하지만, 경우에 따라 NTFS
등의 파일 시스템을 추가하여 확장하는 것 또한 가능하다. filesystem
객체가 추상화되어 있기 때문에 가능하다.
shell_cmd_mount()
int shell_cmd_mount(struct shell *shell, int argc, char *argv[])
{
int result;
if (shell->filesystem.mount == NULL) {
printf("The mount function is NULL\n");
return 0;
}
result = shell->filesystem.mount(
&shell->disk, &shell->fops, &shell->rootdir
);
if (result < 0) {
printf("%s file system mounting has been failed!\n",
shell->filesystem.name);
return -1;
}
printf("%s file system has been mounted successfully\n",
shell->filesystem.name);
shell->curdir = shell->rootdir;
shell->is_mounted = true;
return 0;
}
format
명령어로 초기화한 filesystem
을 mount
한다.
shell_command.c
, (CMD_COND_MOUNT
) CMD_COND_MOUNT
계열 명령어는 앞선 format
과 mount
명령 이후 사용이 가능하다. 이들은 filesystem
그리고 fat
에 종속적인 일부 함수로 전이되지만 이들에 대한 자세한 설명은 생략하고 전체적인 흐름에만 집중할 것이다.
shell_cmd_cd()
int shell_cmd_cd(struct shell *shell, int argc, char *argv[])
{
struct shell_entry new_entry;
shell->path[0] = shell->rootdir;
if (argc > 2) {
printf("usage: %s <directory>\n", argv[0]);
return 0;
}
if (argc == 1) {
shell->path_top = 0;
goto SET_CUR_DIR;
}
if (strcmp(argv[1], ".") == 0) {
return 0;
} else if (strcmp(argv[1], "..") == 0 && shell->path_top > 0) {
shell->path_top--;
} else {
int result = shell->fops.lookup(
&shell->disk, &shell->fops,
&shell->curdir, &new_entry,
argv[1]
);
if ( result != 0) {
printf("directory not found!\n");
return -1;
} else if ( !new_entry.is_dir ) {
printf("%s is not a directory\n", argv[1]);
return -1;
}
shell->path[++shell->path_top] = new_entry;
}
SET_CUR_DIR:
shell->curdir = shell->path[shell->path_top];
return 0;
}
현재 디렉토리를 변경(cd
=> change directory
) 하는 명령어이다. 마지막 else
문을 제외한 나머지 코드는 예외처리 혹은 쉽게 이해 가능한 코드이므로 자세한 설명은 생략토록 하겠다.
마지막 else
문에선 현재 디렉터리를 나타내는 shell->curdir
객체와 인자로 전달된 디렉터리 명을 통해 이동을 원하는 디렉터리가 현재 디렉터리에 존재하는지 확인하다. 이는 lookup()
을 통해 수행된다. 만일 존재한다면 shell->path
에 new_entry
(이동한 새 디렉터리를 나타내는 객체) 를 대입하고 shell->path_top
을 증가시킨다.
현재 디렉터리를 나타내는 shell->curdir
객체를 shell->path[shell->path_top]
객체로 초기화함으로 cd
명령어는 끝이 난다.
shell_cmd_touch()
int shell_cmd_touch(struct shell *shell, int argc, char *argv[])
{
struct shell_entry entry;
int result;
if (argc < 2) {
printf("usage: touch <files...>
");
return 0;
}
result = shell->fops.file_ops.create(
&shell->disk,
&shell->fops,
&shell->curdir,
argv[1],
&entry
);
if ( result != 0 ) {
printf("create failed
");
return -1;
}
return 0;
}
touch
명령어는 인자로 전달된 이름의 빈 파일을 하나 생성한다. 이는 shell->fops.file_ops.create()
를 통해 이뤄진다. 이렇게 복잡한 참조 형식을 가지는 이유는 파일 시스템과 파일에 대한 다양한 연산을 추상화하기 위함이다.
shell_cmd_fill()
리눅스에는 존재하지 않는 명령어인데 파일 시스템의 IO 기능 테스트를 위해 만든 명령어이다.
int shell_cmd_fill(struct shell *shell, int argc, char *argv[])
{
struct shell_entry entry;
int ntimes, wordsiz;
if (argc < 3) {
printf("usage: fill <file> <string> [ntimes]
");
return -1;
} else if (argc == 4) {
if (sscanf(argv[3], "%d", &ntimes) != 1) {
printf("[ntimes] not a number!
");
return -2;
}
} else ntimes = 1;
if (shell->fops.lookup(
&shell->disk, &shell->fops, &shell->curdir, &entry, argv[1]
) != 0) {
printf("cannot find file %s!
", argv[1]);
return -3;
}
wordsiz = strlen(argv[2]);
for (int i = 0; i < ntimes; i++)
shell->fops.file_ops.write(
&shell->disk, &shell->fops, &shell->curdir,
&entry, i * wordsiz, wordsiz, argv[2]
);
return 0;
}
위 명령어가 하는 일은 매우 단순하며 아래와 같다:
shell_cmd_rm()
int shell_cmd_rm(struct shell *shell, int argc, char *argv[])
{
if (argc < 2) {
printf("usage: rm <files...>\n");
return 0;
}
for (int i = 1; i < argc; i++) {
if (shell->fops.file_ops.remove(
&shell->disk, &shell->fops,
&shell->curdir, argv[i]
))
printf("cannot remove file\n");
}
return 0;
}
인자로 전달된 이름의 파일을 삭제한다.
shell_cmd_ls()
int shell_cmd_ls(struct shell *shell, int argc, char *argv[])
{
struct shell_entry_list main;
if (argc > 2) {
printf("usage: %s <path...>\n", argv[0]);
return 0;
}
shell_entry_list_init(&main);
if (shell->fops.read_dir(
&shell->disk, &shell->fops, &shell->curdir, &main
)) {
printf("failed to read_dir()\n");
return -1;
}
printf("%-12s %3s %12s\n", "[File name]", "[D]", "[File size]");
LIST_ITERATOR_WITH_ENTRY(main.head, entry, struct shell_entry, list)
printf("%-12s %3d %12d\n",
entry->name, entry->is_dir, entry->size
);
LIST_ITERATOR_END putchar('\n');
shell_entry_list_release(&main);
return 0;
}
ls
명령어는 현재 디렉터리의 존재하는 모든 파일을 출력하며 이는 아래의 과정을 거친다:
shell_entry_list_init()
함수를 호출하여 main
자료구조의 초기화를 진행한다.shell->fops.read_dir()
함수는 인자로 전달된 연결 리스트(struct shell_entry_list main
) 에 엔트리를 연결하여 반환한다.LIST_ITERATOR_WITH_ENTRY
를 통해 연결 리스트를 순회하며 각 파일들의 정보를 출력한다.shell_entry_list_release()
함수를 호출하여 연결 리스트와 각 파일 엔트리 객체의 자료구조를 해제한다.shell_cmd_df()
int shell_cmd_df(struct shell *shell, int argc, char *argv[])
{
unsigned int used, total;
shell->fops.stat(&shell->disk, &shell->fops, &total, &used);
printf("free sectors: %u (%.2lf%%) "
"used sectors: %u(%.2lf%%) "
"total: %u\n",
total - used, get_percentage(
total - used, shell->disk.number_of_sectors
),
used, get_percentage(
used, shell->disk.number_of_sectors
),
total
);
return 0;
}
df
명령어는 현재 디스크의 사용량을 보여준다.
shell_cmd_mkdir()
int shell_cmd_mkdir(struct shell *shell, int argc, char *argv[])
{
struct shell_entry entry;
int result;
if (argc != 2) {
printf("usage: %s <name>\n", argv[0]);
return 0;
}
result = shell->fops.mkdir(
&shell->disk, &shell->fops, &shell->curdir, argv[1], &entry
);
if (result) {
printf("cannot create directory\n");
return -1;
}
return 0;
}
새로운 디렉터리를 생성한다.
shell_cmd_rmdir()
int shell_cmd_rmdir(struct shell *shell, int argc, char *argv[])
{
int result;
if (argc != 2) {
printf("usage: %s <name>\n", argv[0]);
return 0;
}
result = shell->fops.rmdir(
&shell->disk, &shell->fops, &shell->curdir, argv[1]
);
if ( result != 0 ) {
printf("cannot remove directory\n");
return -1;
}
return 0;
}
디렉터리를 제거한다.
shell_cmd_cat()
int shell_cmd_cat(struct shell *shell, int argc, char *argv[])
{
struct shell_entry entry;
char buffer[BUFSIZ];
int result;
unsigned long offset;
if (argc != 2) {
printf("usage: %s <file>\n", argv[0]);
return 0;
}
result = shell->fops.lookup(
&shell->disk, &shell->fops, &shell->curdir, &entry, argv[1]
);
if ( result != 0 ) {
printf("%s lookup failed!\n", argv[1]);
return -1;
}
offset = 0;
while (shell->fops.file_ops.read(
&shell->disk, &shell->fops, &shell->curdir,
&entry, offset, BUFSIZ, buffer) > 0) {
printf("%s", buffer);
offset += BUFSIZ;
memset(buffer, 0x00, BUFSIZ);
}
putchar('\n');
return 0;
}
파일의 내용물을 출력한다.