本文分爲兩部分:
第一部分將詳細分析JOS
的文件系統及文件描述符的實現方法。
第二部分將實現工作路徑,提供新的系統調用,完善用戶空間工具。
本文中支持的新特性:
支持進程工作目錄 提供
getcwd
與chdir
新的
syscall
SYS_env_set_workpath
修改工作路徑
- 新的用戶程序
ls
功能完善pwd
輸出當前工作目錄cat
接入工作目錄touch
由於文件屬性沒啥可改的,用於創建文件mkdir
創建目錄文件msh
更高級的shell
還未完全完工 支持cd
支持默認二進制路徑爲bin
- 調整目標磁盤生成工具
Github:https://github.com/He11oLiu/MOS
JOS文件系統詳解
文件系統總結
Regular env FS env
+---------------+ +---------------+
| read | | file_read |
| (lib/fd.c) | | (fs/fs.c) |
...|.......|.......|...|.......^.......|...............
| v | | | | RPC mechanism
| devfile_read | | serve_read |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| fsipc | | serve |
| (lib/file.c) | | (fs/serv.c) |
| | | | ^ |
| v | | | |
| ipc_send | | ipc_recv |
| | | | ^ |
+-------|-------+ +-------|-------+
| |
+-------------------+
- 底層與磁盤有關的丟給
ide_xx
來實現,因爲要用到IO
中斷,要給對應的權限 - 通過
bc_pgfault
來實現缺頁自己映射,利用flush_block
來回寫磁盤 - 然後通過分
block
利用block cache
實現對於磁盤的數據讀入內存或者寫回磁盤 - 再上面一層的
file_read
與file_write
,均是對於blk
的操作。 - 再上面就是文件系統服務器,通過調用
file_read
實現功能了。 - 客戶端通過對需求打包,發送
IPC
給文件系統服務器,即可實現讀/寫文件的功能。
文件系統&文件描述符 Overview
JOS文件系統是直接映射到內存空間DISKMAP
到DISKMAP + DISKSIZE
這塊空間。故其支持的文件系統最大爲3GB.
IDE ide.c
文件系統底層PIO
驅動放在ide.c
中。注意在IDE
中,是以硬件的角度來看待硬盤,其基本單位是sector
,不是block
。
bool ide_probe_disk1(void)
用於檢測disk1
是否存在。voidide_set_disk(int diskno)
用於設置目標磁盤。ide_read ide_write
用於磁盤讀寫。
block cache bc.c
文件系統在內存中的映射是基於block cache
的。以一個block
爲單位在內存中爲其分配單元。注意在bc
中,是以操作系統的角度來看待硬盤,其基本單位是block
,不是sector
。
void *diskaddr(uint32_t blockno)
用於查找blockno
在地址空間中的地址。blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE
用於查找addr
對應文件系統中的blockno
。static void bc_pgfault(struct UTrapframe *utf)
用於處理讀取不在內存中而出現page fault
的情況。這時需要從file system
通過PIO
讀取到block cache
(也就是內存中新分配的一頁)中,並做好映射。void flush_block(void *addr)
用於寫回硬盤,寫回時清理PTE_D
標記。
file system fs.c
文件系統是基於剛纔的block cache
和底層ide
驅動的。
bitmap 相關
bitmap
每一位代表着一個block
的狀態,用位操作檢查/設置block
狀態即可。
bool block_is_free(uint32_t blockno)
用於check給定的blockno
是否是空閒的。void free_block(uint32_t blockno)
設置對應位爲0int alloc_block(void)
設置對應位爲1
文件系統操作
void fs_init(void)
初始化文件系統。檢測disk1
是否存在,檢測super block
和bitmap block
。static int file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
用於找到文件f
的fileno
個block
的blockno
。alloc
用於控制當f_indirect
不存在的時候,是否需要新申請一個block
。int file_get_block(struct File *f, uint32_t filebno, char **blk)
用於找到文件f
的fileno
個block
的地址。static int dir_lookup(struct File *dir, const char *name, struct File **file)
用於在dir
下查找name
這個文件。其遍歷讀取dir
這個文件,並逐個判斷其目錄下每一個文件的名字是否相等。static int dir_alloc_file(struct File *dir, struct File **file)
在dir
下新申請一個file
。同樣也是遍歷所有的dir
下的文件。找到第一個名字爲空的文件,並把新的文件存在這裏。static int walk_path(const char *path, struct File **pdir, struct File **pf, char *lastelem)
用於從根目錄獲取path
的文件,文件放在pf
中,路徑放在pdir
中。如果找到了路徑沒有找到文件。最後的路徑名放在lastelem
中,最後的路徑放在pdir
中。
文件操作
int file_create(const char *path, struct File **pf)
用於創建文件。int file_open(const char *path, struct File **pf)
打開文件。ssize_t file_read(struct File *f, void *buf, size_t count, off_t offset)
從f
的offset
讀取count
bytes的數據放入buf
中。int file_write(struct File *f, const void *buf, size_t count, off_t offset)
與上面的類似。static int file_free_block(struct File *f, uint32_t filebno)
刪除文件中的filebno
static void file_truncate_blocks(struct File *f, off_t newsize)
縮短文件大小。int file_set_size(struct File *f, off_t newsize)
修改文件大小。void file_flush(struct File *f)
將文件寫回硬盤void fs_sync(void)
將所有的文件寫回硬盤
文件系統服務器 serv.c
服務器主要邏輯
umain
: 初始化文件系統,初始化服務器,開始接收請求。服務器具體函數見上面實現。
int openfile_alloc(struct OpenFile **o)
用於服務器分配一個openfile
結構體
文件描述符 fd.c
struct fd
結構體struct Fd { int fd_dev_id; off_t fd_offset; int fd_omode; union { // File server files struct FdFile fd_file; }; };
其中
fd_file
用於發送的時候傳入服務器對應的fileid
包括了
fd_id
文件讀取的offset
,讀取模式以及FdFile
int fd2num(struct Fd *fd)
從fd
獲取其編號char* fd2data(struct Fd *fd)
從fd
獲取文件內容int fd_alloc(struct Fd **fd_store)
查找到第一個空閒的fd
,並分配出去。int fd_lookup(int fdnum, struct Fd **fd_store)
爲查找fdnum
的fd,並放在fd_store
中。int fd_close(struct Fd *fd, bool must_exist)
用於關閉並free一個fdint dev_lookup(int dev_id, struct Dev **dev)
獲取不同的Deviceint close(int fdnum)
關閉fd
void close_all(void)
關閉全部int dup(int oldfdnum, int newfdnum)
dup
不是簡單的複製,而是要將兩個fd
的內容完全同步,其是通過虛擬內存映射做到的。read(int fdnum, void *buf, size_t n)
後面的與這個類似- 獲取
fd
的fd_dev_id
並根據其獲取dev
- 調用
dev
對應的function
- 獲取
int seek(int fdnum, off_t offset)
用於設置fd
的offset
int fstat(int fdnum, struct Stat *stat)
獲取文件狀態。struct Stat { char st_name[MAXNAMELEN]; off_t st_size; int st_isdir; struct Dev *st_dev; };
int stat(const char *path, struct Stat *stat)
獲取路徑狀態。
具體關於文件描述符的設計見下圖。
下面就來詳細看現有的三個device
文件系統讀寫 file.c
之前已經分析過
devfile_xx
的函數static int fsipc(unsigned type, void *dstva)
用於給文件系統服務器發送IPC
這裏是實例化了一個用於文件讀取的
dev
struct Dev devfile = { .dev_id = 'f', .dev_name = "file", .dev_read = devfile_read, .dev_close = devfile_flush, .dev_stat = devfile_stat, .dev_write = devfile_write, .dev_trunc = devfile_trunc };
管道 pipe.c
關於pipe
管道是一種把兩個進程之間的標準輸入和標準輸出連接起來的機制,從而提供一種讓多個進程間通信的方法,當進程創建管道時,每次都需要提供兩個文件描述符來操作管道。其中一個對管道進行寫操作,另一個對管道進行讀操作。對管道的讀寫與一般的IO系統函數一致,使用write()函數寫入數據,使用read()讀出數據。
同剛纔的file
的操作類似,這裏是對於pipe
的操作。
struct Dev devpipe =
{
.dev_id = 'p',
.dev_name = "pipe",
.dev_read = devpipe_read,
.dev_write = devpipe_write,
.dev_close = devpipe_close,
.dev_stat = devpipe_stat,
};
pipe 的結構體如下
struct Pipe
{
off_t p_rpos; // read position
off_t p_wpos; // write position
uint8_t p_buf[PIPEBUFSIZ]; // data buffer
};
int pipe(int pfd[2])
申請兩個新的fd
,映射到同一個虛擬地址上,一邊Read_only
一邊Write_only
即可。
static ssize_t devpipe_read(struct Fd *fd, void *vbuf, size_t n)
其從fd
對應的data
獲取pipe
。p = (struct Pipe *)fd2data(fd);
然後從pipe->buf
中讀取內容。維護p_rpos
。static ssize_t devpipe_write(struct Fd *fd, const void *vbuf, size_t n)
其從fd
對應的data
獲取pipe
。p = (struct Pipe *)fd2data(fd);
然後向pipe->buf
中寫入內容。維護p_wpos
。
屏幕輸入輸出 console.c
struct Dev devcons =
{
.dev_id = 'c',
.dev_name = "cons",
.dev_read = devcons_read,
.dev_write = devcons_write,
.dev_close = devcons_close,
.dev_stat = devcons_stat};
實現直接調用syscall
即可,和之前實現的putchar
類似。
支持工作路徑以及更完整的工具
本本分將主要關注用戶空間程序,並補全內核功能(支持工作路徑)。
本部分主要包括以下用戶應用程序:
ls list directory contents
pwd return working directory name
mkdir make directories
touch change file access and modification times(we only support create file)
cat concatenate and print files
shell
list directory contents
讀文件
由於寫到這裏第一次在用戶空間讀取文件,簡要記錄一下讀取文件的過程。
首先是文件結構,在lab5中設計文件系統的時候設計的,保存在struct File
中,用戶可以根據此結構體偏移來找具體的信息。
再是fsformat
中提供的與文件系統相關的接口。這裏用到了readn
。其只是對於read
的一層包裝。
功能實現
回到ls
本身的邏輯上。ls
主要是讀取path
文件,並將其下所有的文件名全部打印出來。
return working directory name
由於之前寫的JOS
中每個進程沒有寫工作目錄。這裏再加上工作目錄。
在struct env
中加入工作目錄,添加後env
如下:
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
int env_cpunum; // The CPU that the env is running on
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
// Exception handling
void *env_pgfault_upcall; // Page fault upcall entry point
// IPC
bool env_ipc_recving; // Env is blocked receiving
void *env_ipc_dstva; // VA at which to map received page
uint32_t env_ipc_value; // Data value sent to us
envid_t env_ipc_from; // envid of the sender
int env_ipc_perm; // Perm of page mapping received
// work path
char workpath[MAXPATH];
};
由於env
對於用戶是不可以寫的,所以要添加新的syscall
,進入內核態改。
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
SYS_page_alloc,
SYS_page_map,
SYS_page_unmap,
SYS_exofork,
SYS_env_set_status,
SYS_env_set_trapframe,
SYS_env_set_pgfault_upcall,
SYS_yield,
SYS_ipc_try_send,
SYS_ipc_recv,
SYS_getcwd,
SYS_chdir,
NSYSCALLS
};
由於JOS
中用戶其實可以讀env
中的內容,所以getcwd
就不陷入內核態了,直接讀取就好。
新建dir.c
用於存放與目錄有關的函數,實現getcwd
char *getcwd(char *buffer, int maxlen)
{
if(!buffer || maxlen < 0)
return NULL;
return strncpy((char *)buffer,(const char*)thisenv->workpath,maxlen);
}
而對於修改目錄,必須要陷入內核態了,新加syscall
。
int sys_chdir(const char *path)
{
return syscall(SYS_chdir, 0, (uint32_t)path, 0, 0, 0, 0);
}
剛纔的dir.c
中加入用戶接口
// change work path
// Return 0 on success,
// Return < 0 on error. Errors are:
// -E_INVAL *path not exist or not a path
int chdir(const char *path)
{
int r;
struct Stat st;
if ((r = stat(path, &st)) < 0)
return r;
if(!st.st_isdir)
return -E_INVAL;
return sys_chdir(path);
}
然後去內核添加功能
// change work path
// return 0 on success.
static int
sys_chdir(const char * path)
{
strcpy((char *)curenv->workpath,path);
return 0;
}
最後實現pwd
#include <inc/lib.h>
void umain(int argc, char **argv)
{
char path[200];
if(argc > 1)
printf("%s : too many arguments\n",argv[0]);
else
printf("%s\n",getcwd(path,200));
}
make directories
發現JOS
給我們預留了標識位O_MKDIR
,由於與普通的file_create
不一樣,當有同名的文件存在的時候,但其不是目錄的情況下,我們仍然可以創建,所以新寫了函數
int dir_create(const char *path, struct File **pf)
{
char name[MAXNAMELEN];
int r;
struct File *dir, *f;
if (((r = walk_path(path, &dir, &f, name)) == 0) &&
f->f_type == FTYPE_DIR)
return -E_FILE_EXISTS;
if (r != -E_NOT_FOUND || dir == 0)
return r;
if ((r = dir_alloc_file(dir, &f)) < 0)
return r;
// fill struct file
strcpy(f->f_name, name);
f->f_type = FTYPE_DIR;
*pf = f;
file_flush(dir);
return 0;
}
然後在serve_open
下建立新的分支
// create dir
else if (req->req_omode & O_MKDIR)
{
if ((r = dir_create(path, &f)) < 0)
{
if (!(req->req_omode & O_EXCL) && r == -E_FILE_EXISTS)
goto try_open;
if (debug)
cprintf("file_create failed: %e", r);
return r;
}
}
在dir.c
下提供mkdir
函數
// make directory
// Return 0 on success,
// Return < 0 on error. Errors are:
// -E_FILE_EXISTS directory already exist
int mkdir(const char *dirname)
{
char cur_path[MAXPATH];
int r;
getcwd(cur_path, MAXPATH);
strcat(cur_path, dirname);
if ((r = open(cur_path, O_MKDIR)) < 0)
return r;
close(r);
return 0;
}
最後提供用戶程序
#include <inc/lib.h>
#define MAXPATH 200
void umain(int argc, char **argv)
{
int r;
if (argc != 2)
{
printf("usage: mkdir directory\n");
return;
}
if((r = mkdir(argv[1])) < 0)
printf("%s error : %e\n",argv[0],r);
}
Create file
創建文件直接利用open
中的O_CREAT
選項即可。
#include <inc/lib.h>
#define MAXPATH 200
void umain(int argc, char **argv)
{
int r;
char *filename;
char pathbuf[MAXPATH];
if (argc != 2)
{
printf("usage: touch filename\n");
return;
}
filename = argv[1];
if (*filename != '/')
getcwd(pathbuf, MAXPATH);
strcat(pathbuf, filename);
if ((r = open(pathbuf, O_CREAT)) < 0)
printf("%s error : %e\n", argv[0], r);
close(r);
}
cat
這個只需要修改好支持工作路徑即可
#include <inc/lib.h>
char buf[8192];
void cat(int f, char *s)
{
long n;
int r;
while ((n = read(f, buf, (long)sizeof(buf))) > 0)
if ((r = write(1, buf, n)) != n)
panic("write error copying %s: %e", s, r);
if (n < 0)
panic("error reading %s: %e", s, n);
}
void umain(int argc, char **argv)
{
int f, i;
char *filename;
char pathbuf[MAXPATH];
binaryname = "cat";
if (argc == 1)
cat(0, "<stdin>");
else
for (i = 1; i < argc; i++)
{
filename = argv[1];
if (*filename != '/')
getcwd(pathbuf, MAXPATH);
strcat(pathbuf, filename);
f = open(pathbuf, O_RDONLY);
if (f < 0)
printf("can't open %s: %e\n", argv[i], f);
else
{
cat(f, argv[i]);
close(f);
}
}
}
SHELL
寫Shell
的時候發現問題:之前沒有解決fork
以及spawn
時候的子進程的工作路徑的問題。所有再一次修改了系統調用,將系統調用sys_chdir
修改爲能夠設定指定進程的工作目錄的系統調用。
int sys_env_set_workpath(envid_t envid, const char *path);
修改對應的內核處理:
// change work path
// return 0 on success.
static int
sys_env_set_workpath(envid_t envid, const char *path)
{
struct Env *e;
int ret = envid2env(envid, &e, 1);
if (ret != 0)
return ret;
strcpy((char *)e->workpath, path);
return 0;
}
這樣就會fork
出來的子進程繼承父親的工作路徑。
在shell
中加入built-in
功能,爲未來擴展shell
功能提供基礎
int builtin_cmd(char *cmdline)
{
int ret;
int i;
char cmd[20];
for (i = 0; cmdline[i] != ' ' && cmdline[i] != '\0'; i++)
cmd[i] = cmdline[i];
cmd[i] = '\0';
if (!strcmp(cmd, "quit") || !strcmp(cmd, "exit"))
exit();
if (!strcmp(cmd, "cd"))
{
ret = do_cd(cmdline);
return 1;
}
return 0;
}
int do_cd(char *cmdline)
{
char pathbuf[BUFSIZ];
int r;
pathbuf[0] = '\0';
cmdline += 2;
while (*cmdline == ' ')
cmdline++;
if (*cmdline == '\0')
return 0;
if (*cmdline != '/')
{
getcwd(pathbuf, BUFSIZ);
}
strcat(pathbuf, cmdline);
if ((r = chdir(pathbuf)) < 0)
printf("cd error : %e\n", r);
return 0;
}
修改<
與 >
支持當前工作路徑
case '<': // Input redirection
// Grab the filename from the argument list
if (gettoken(0, &t) != 'w')
{
cprintf("syntax error: < not followed by word\n");
exit();
}
// Open 't' for reading as file descriptor 0
// (which environments use as standard input).
// We can't open a file onto a particular descriptor,
// so open the file as 'fd',
// then check whether 'fd' is 0.
// If not, dup 'fd' onto file descriptor 0,
// then close the original 'fd'.
if (t[0] != '/')
getcwd(argv0buf, MAXPATH);
strcat(argv0buf, t);
if ((fd = open(argv0buf, O_RDONLY)) < 0)
{
cprintf("Error open %s fail: %e", argv0buf, fd);
exit();
}
if (fd != 0)
{
dup(fd, 0);
close(fd);
}
break;
case '>': // Output redirection
// Grab the filename from the argument list
if (gettoken(0, &t) != 'w')
{
cprintf("syntax error: > not followed by word\n");
exit();
}
if (t[0] != '/')
getcwd(argv0buf, MAXPATH);
strcat(argv0buf, t);
if ((fd = open(argv0buf, O_WRONLY | O_CREAT | O_TRUNC)) < 0)
{
cprintf("open %s for write: %e", argv0buf, fd);
exit();
}
if (fd != 1)
{
dup(fd, 1);
close(fd);
}
break;
創建硬盤鏡像
利用
mmap
映射到內存,對內存讀寫。if ((diskmap = mmap(NULL, nblocks * BLKSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, diskfd, 0)) == MAP_FAILED) panic("mmap %s: %s", name, strerror(errno));
從
diskmap
開始,大小爲nblocks * BLKSIZE
alloc
用於分配空間,移動diskpos
void *
alloc(uint32_t bytes)
{
void *start = diskpos;
diskpos += ROUNDUP(bytes, BLKSIZE);
if (blockof(diskpos) >= nblocks)
panic("out of disk blocks");
return start;
}
塊 123 在初始化的時候分配
alloc(BLKSIZE); super = alloc(BLKSIZE); super->s_magic = FS_MAGIC; super->s_nblocks = nblocks; super->s_root.f_type = FTYPE_DIR; strcpy(super->s_root.f_name, "/"); nbitblocks = (nblocks + BLKBITSIZE - 1) / BLKBITSIZE; bitmap = alloc(nbitblocks * BLKSIZE); memset(bitmap, 0xFF, nbitblocks * BLKSIZE);
writefile
用於申請空間,寫入磁盤void writefile(struct Dir *dir, const char *name) { int r, fd; struct File *f; struct stat st; const char *last; char *start; if ((fd = open(name, O_RDONLY)) < 0) panic("open %s: %s", name, strerror(errno)); if ((r = fstat(fd, &st)) < 0) panic("stat %s: %s", name, strerror(errno)); if (!S_ISREG(st.st_mode)) panic("%s is not a regular file", name); if (st.st_size >= MAXFILESIZE) panic("%s too large", name); last = strrchr(name, '/'); if (last) last++; else last = name; // 獲取目錄中的一個空位 f = diradd(dir, FTYPE_REG, last); // 獲取文件存放地址,分配空間 start = alloc(st.st_size); // 將文件讀如到磁盤中剛剛分配的地址 readn(fd, start, st.st_size); // 完成文件信息 finishfile(f, blockof(start), st.st_size); close(fd); } void finishfile(struct File *f, uint32_t start, uint32_t len) { int i; // 這個是剛纔目錄下傳過來的地址,直接修改目錄下的相應項 f->f_size = len; len = ROUNDUP(len, BLKSIZE); for (i = 0; i < len / BLKSIZE && i < NDIRECT; ++i) f->f_direct[i] = start + i; if (i == NDIRECT) { uint32_t *ind = alloc(BLKSIZE); f->f_indirect = blockof(ind); for (; i < len / BLKSIZE; ++i) ind[i - NDIRECT] = start + i; } }
目錄結構體與何時將目錄寫入
void startdir(struct File *f, struct Dir *dout) { dout->f = f; dout->ents = malloc(MAX_DIR_ENTS * sizeof *dout->ents); dout->n = 0; } void finishdir(struct Dir *d) { // 目錄文件的大小 int size = d->n * sizeof(struct File); // 申請目錄文件存放空間 struct File *start = alloc(size); // 將目錄的文件內容放進去 memmove(start, d->ents, size); // 補全目錄在磁盤當中的信息 finishfile(d->f, blockof(start), ROUNDUP(size, BLKSIZE)); free(d->ents); d->ents = NULL; }
添加
bin
路徑,並在shell
中類似path
環境變量默認讀取bin
下的可執行文件opendisk(argv[1]); startdir(&super->s_root, &root); f = diradd(&root, FTYPE_DIR, "bin"); startdir(f,&bin); for (i = 3; i < argc; i++) writefile(&bin, argv[i]); finishdir(&bin); finishdir(&root); finishdisk();
獲取時間
又新增一個syscall
,這裏不再累述,利用mc146818_read
獲取cmos
時間即可。
int gettime(struct tm *tm)
{
unsigned datas, datam, datah;
int i;
tm->tm_sec = BCD_TO_BIN(mc146818_read(0));
tm->tm_min = BCD_TO_BIN(mc146818_read(2));
tm->tm_hour = BCD_TO_BIN(mc146818_read(4)) + TIMEZONE;
tm->tm_wday = BCD_TO_BIN(mc146818_read(6));
tm->tm_mday = BCD_TO_BIN(mc146818_read(7));
tm->tm_mon = BCD_TO_BIN(mc146818_read(8));
tm->tm_year = BCD_TO_BIN(mc146818_read(9));
return 0;
}
實機運行輸出
check_page_free_list() succeeded!
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
====Graph mode on====
scrnx = 1024
scrny = 768
MMIO VRAM = 0xef803000
=====================
SMP: CPU 0 found 1 CPU(s)
enabled interrupts: 1 2 4
FS is running
FS can do I/O
Device 1 presence: 1
block cache is good
superblock is good
bitmap is good
# msh in / [12: 4:28]
$ cd documents
# msh in /documents/ [12: 4:35]
$ echo hello liu > hello
# msh in /documents/ [12: 4:45]
$ cat hello
hello liu
# msh in /documents/ [12: 4:49]
$ cd /bin
# msh in /bin/ [12: 4:54]
$ ls -l -F
- 37 newmotd
- 92 motd
- 447 lorem
- 132 script
- 2916 testshell.key
- 113 testshell.sh
- 20308 cat
- 20076 echo
- 20508 ls
- 20332 lsfd
- 25060 sh
- 20076 hello
- 20276 pwd
- 20276 mkdir
- 20280 touch
- 29208 msh
# msh in /bin/ [12: 4:57]
$