• CS162 shell


    本文记录我在做shell这个作业时用到有关资源,如Linux系统调用、Linux基础知识、C语言知识等。

    这里只是非常简略地记录了一下,并且可能有理解不正确的地方,你可以把本文当作一个索引和没有思路时的启发,详细的信息可以再去查,我也给出了一些可能会用到的文档链接。

    另一方面,我不想这篇文章干扰了你的思路,如果你发现你完全没有用我说到的东西就实现了功能,那也不要怀疑自己,毕竟,shell肯定有多种实现的方法。

    判分问题:我在判分的时候一直是8分,直到完成了signal handling后才变成了9分,所以不要担心分数为什么一直不变。

    对于各个系统调用,推荐你看官方的Linux manual或者可以在命令行直接用man查看。

    support for cd and pwd

    chdir

    #include
    int chdir(const char *path);
    int fchdir(int fd);
    
    • 1
    • 2
    • 3

    chdir()改变当前进程的工作目录到path指定的位置处。

    fchdir()与前者唯一不同的是传入的参数是一个打开文件描述符open file descriptor

    返回值:若成功则返回0,否则返回-1,并且errno被设置以反映错误。

    通过fork()创建的子进程继承父进程当前的目录。

    getcwd

    #include
    char *getcwd(char *buf, size_t size);
    char *getwd(char *buf);
    char *get_current_dir_name(void);
    
    • 1
    • 2
    • 3
    • 4

    返回一个包含绝对路径的以null结尾的字符串,该路径即当前进程的工作目录,该值通过返回值和参数buf返回(如果有)。

    1. getcwd:如果路径长度超过了size,则返回NULL,并且errno设置为ERANGE,程序应该检查该错误,若需要,可以分配一个更大的buf
    2. get_currrent_dir_name:会申请一个足够大的空间存放目录,如果环境变量PWD被设置了,并且其值正确,则返回PWD,使用者应该手动释放buf

    errno

    #include
    
    • 1

    表示上次错误的一个数字int,由系统调用(system call)设置,不同的数字可以表示不同的错误,其值都是正数。

    tokens

    struct tokens {
        size_t tokens_length;
        char** tokens;
        size_t buffers_length;
        char** buffers;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    存放目录字符的结构体,其中包括一个缓冲区buffers

    函数指针cmd_fun_t

    typedef int cmd_fun_t(struct tokens* tokens);
    
    • 1

    该行代码定义了一个函数指针cmd_fun_t,其指向一个函数,参数为tokens*,返回值类型为int

    unused

    该关键字标识的参数可能在函数中未使用。

    getenv

    #include 
    
    char *getenv(const char *name);
    
    • 1
    • 2
    • 3

    getenv("HOME")可以得到/home/username目录。

    program execution

    C语言的 …(ellipse)语法

    函数参数最后可以使用...,这个是为了让函数可以传入可变数量的参数,下面是一个例子:

    int a_function(int x, ...) {
        va_list list;	// 存放参数的列表
        va_start(list, x);	// 初始化list
        va_arg(list, int); // 返回list中的第一个参数,以int形式返回
        va_arg(list, int); // 返回list中的第二个参数,以int形式返回
        ...					// 不断地取
        va_end(list);// 取完后清空list
    }
    
    a_function(3, 1, 2, 3);  // 调用函数,可以传入不同数量的参数
    a_function(5, 2, 9, 1, 8, 7);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    要注意的是必须明确地知道每个参数的类型,才能保证取数的正确。

    exec

    include <unistd.h>
    extern char **environ;
    
    int execl(const char *pathname, const char *arg, ...
              /*, (char *) NULL */);
    int execlp(const char *file, const char *arg, ...
               /*, (char *) NULL */);
    int execle(const char *pathname, const char *arg, ...
               /*, (char *) NULL, char *const envp[] */);
    int execv(const char *pathname, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在任务2中不使用execvp

    exec族的函数将当前进程替换为pathnamefile指定的进程,其中arg, ...argv[]存放参数,例如,对于ls来说,-a-l就是其参数,要注意的是,argv[0]需要放路径,如对于执行ls,可以这样写:

    char ** args = { "/bin/ls", "-a", "-s", NULL };
    execl(args[0], args[0], args[1], args[2], args[3]);
    // 或
    execv(args[0], args);
    
    • 1
    • 2
    • 3
    • 4

    symbolic link

    我认为可以将symbolic link当作引用理解。

    创建链接

    linux中可以创建链接,每个链接指向一个地址,对该链接的修改等同于对原地址内容的修改。

    ln -s /home/transactions.txt school/trans.txt
    
    • 1

    上面的命令创建了一个链接school/trans.txt,对trans.txt的修改即对transactions.txt的修改。

    要注意的是链接所在的文件夹(本例中为school)在创建连接前必须已经创建,否则报错。

    也可以对文件夹创建链接:

    ln -s /home/junhao junhao
    
    • 1

    链接中会包含所有原文件夹的内容,对链接中文件的修改也会反映到原文件夹。

    删除链接

    首先可以使用如下命令检查某个文件是否为链接,如果为链接,可以看到有xxx -> xxx形式的输出。

    ls -l pathname
    
    • 1

    例子如下:

    可以使用如下命令删除链接:

    unlink linkname
    
    • 1

    stat

    #include 
    
    int stat(const char *restrict pathname,
             struct stat *restrict statbuf);
    
    • 1
    • 2
    • 3
    • 4

    该函数返回文件pathname的相关信息,存放在statbuf中,

    fork

    #include 
    
    pid_t fork(void);
    
    • 1
    • 2
    • 3

    该函数用于创建子进程,如果创建子进程成功,则函数在子进程中返回0,在父进程中返回子进程的pid,如果失败则在父进程中返回-1,并且errno被设置。

    wait

    #include 
    
    pid_t wait(int *wstatus);
    pid_t waitpid(pid_t pid, int *wstatus, int options);
    
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    /* This is the glibc and POSIX interface; see
                              NOTES for information on the raw system call. */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    wait族的函数用于等待子进程状态的改变,并且获取其信息,状态改变有以下几种情况:

    1. 子进程终止
    2. 子进程暂停
    3. 子进程恢复运行

    wait()将父进程挂起,直到其子进程之一终止,其中wait(&wstatus)waitpid(-1, &wstatus, 0)作用一致。

    waitpid()将父进程挂起,直到由pid指定的子进程状态改变,默认地,该方法等待子进程终止,可以通过设置参数options指定子进程的行为。

    exit vs kill

    exit是进程自己结束,而kill是进程关闭其他进程。

    gdb debug在父子进程间切换

    为了对子进程进行debug,需要在gdb中输入以下命令:

    (gdb) set follow-fork-mode child
    
    • 1

    输入如下命令切换回父进程:

    (gdb) set follow-fork-mode parent
    
    • 1

    在shell中启动另一个shell产生SIGTTIN报错

    这个问题在后面的Signal Handling部分中会被解决,目前可以不管。

    process group

    若干个进程组成一组,指向该组的信号量可以统一控制该组中的所有进程。每个组有一个id,值与创建该组的进程的id一致。

    fork出来的子进程与父进程在同一组中。

    file descriptor

    文件描述符是一个数字,为一个已打开文件的唯一标识。

    isatty

    #include 
    
    int isatty(int fd);
    
    • 1
    • 2
    • 3

    Path resolution

    access

    #include 
    
    int access(const char *pathname, int mode);
    
    • 1
    • 2
    • 3

    检查当前结成是否可以访问pathname文件,mode中设置检查的方式,其值可以为:

    1. F_OK:检查文件是否存在。
    2. 由R_OK、W_OK和X_OK中的若干个按或(OR)运算得到的掩码:检查文件是否存在,并且赋予读(R_OK)、写(W_OK)和执行权限(X_OK)。

    返回值:若成功(文件存在,授予权限成功),则返回0,若失败,返回-1。

    strtok和strdup

    在解析$PATH的时候我用了strtok()进行字符串,结果这个东西直接把我的$PATH给改了。。。,为了不让strtok()修改$PATH,把用getenv()得到的$PATH用strdup进行复制,用复制品进行解析。

    除此之外,还要注意strtok()不会申请新的空间,因此不需要free,珍爱生命,远离strtok()

    这个strdup()strcpy()差不多,都是复制,不同的是,它可以自动malloc()一段内存,不用自己申请了,但是依然要手动free

    Redirection

    IO Redrection

    输入输出重定向的介绍和例子

    实现思路

    read write

    #include 
    ssize_t read(int fd, void *buf, size_t count);
    
    • 1
    • 2

    从文件描述符fd指向的文件中读出count字节的数据,放入缓冲buf中。如果成功,则返回读取的字节数,并且文件中的位置向前那么多。若失败,则返回-1。

    #include 
    ssize_t write(int fd, const void *buf, size_t count);
    
    • 1
    • 2

    write与read类似。

    dup

    #include 
    
    int dup(int oldfd);
    int dup2(int oldfd, int newfd);
    
    • 1
    • 2
    • 3
    • 4

    dup()创建一个新的文件描述符,其指向的文件与oldfd相同,新的文件描述符的值是未被使用的值中的最小值。

    dup2()的功能与dup()相同,区别是令描述符newfd指向oldfd指向的那个文件。

    若成功则返回新描述符的值,失败则返回-1

    open

    #include 
    
    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4

    open()打开文件pathname,若不存在,则会选择性地进行创建文件(若flags中有O_CREAT则创建),flags由若干个标识符的按位或运算组成,如O_CERAT | O_RDONLY

    如果flags中没有O_CREATO_TMPFILE,那么mode会被忽略,否则,其必须存在,

    若成功,返回文件描述符,若失败,返回-1。

    除此之外,你可能会用到creat(pathname, mode),并看到mode值是0600,这个数为0400 | 0200的结果,即创建文件的用户拥有对该文件的读写权限。

    close

    #include 
    
    int close(int fd);
    
    • 1
    • 2
    • 3

    关闭一个文件描述符,让它不再指向任何文件且不再被使用。若成功,返回0,若失败,返回1。

    linux文件类型

    有如下3类文件:

    uid

    即user identifier,linux上的每个用户都有一个唯一的标识符。使用getuid()可以查看当前进程的uid。

    uid的介绍

    Pipes

    实现思路

    pipe

    #include 
    int pipe(int pipefd[2]);
    
    • 1
    • 2

    创建一个数据单向流通的管道,用于进程间的交互。pipefd数组存放返回的2个文件描述符,pipefd[0]指向最后读取的文件,而pipefd[1]指向最后被写入的文件。

    若成功,则返回0,若失败,则返回1。

    优先级问题

    这里有一个redirectionpipes的优先级问题,这可能是个有用的链接:pipe-redirection-precedence。下图也大致说明了二者的优先级。

    关闭不用的文件描述符

    这个非常重要,如果不关闭,子进程可能陷入一直等待输入的状态。如果你发现子进程无法终止,很可能是这个问题。

    具体地,你可以看:

    1. 香港中文大学的小实验的2.2节
    2. CS 162的Section 3的答案
    3. 前面实现思路的链接中也有说明这个问题。

    Signal handling

    老师作业文档里的那个tutorial链接很有用,值得一看,这里就不重复给链接了。

    getpgid和getpgrp

    #include 
    pid_t getpgid(pid_t pid);
    pid_t getpgrp(void);
    
    • 1
    • 2
    • 3

    返回id为pid的进程所在的组的group id,如果pid为0,则返回调用该方法的进程所在组的group id,若失败,则返回-1。

    getpgrp()getpgid(0)作用相同。

    setpgid和setpgrp

    #include 
    int setpgid(pid_t pid, pid_t pgid);
    pid_t sepgrp(void);
    
    • 1
    • 2
    • 3

    将id为pid的进程所属的组切换为pgid,同理,若pid为0,则调用该方法的进程的组切换为pgid

    setpgrp()setpgid(0, 0)作用相同。

    tcgetpgrp和tcsetpgrp

    #include 
    pid_t tcgetpgrp(int fd);
    int tcsetpgrp(int fd, pid_t pgrp);
    
    • 1
    • 2
    • 3

    tcgetpgrp()返回前台进程组的group id。

    tcsetpgrp()将前台进程组设为pgrp那组,若fd为0,则用以控制标准输出。

    对于以上内容,可以查看手册和下图:

    signal

    每个信号(signal)都有一个当前的配置(disposition),其决定了进程接收到该信号时所执行的行为,具体可看文档。

    sigaction

    #include 
    int sigaction(int signum, const struct sigaction *restrict act,
                  struct sigaction *restrict oldact);
    
    • 1
    • 2
    • 3

    该函数修改进程收到指定的某个信号(signal)时所执行的行为。其中:

    1. signum为指定的信号,可以是除了SIGKILLSIGSTOP外的所有有效信号。
    2. act中用于指定新的动作,不为NULL,否则无法指定新动作。
    3. oldact用于存放旧的动作,不为NULL,否则无法存放旧动作。

    其中struct sigaction如下所示:

    struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    YC-Framework版本更新:V1.0.10
    DAO 的未来:构建 web3 的组织原语
    19、Flink 的Table API 和 SQL 中的内置函数及示例(1)
    一个iOS tableView 滚动标题联动效果的实现
    编译linux内核模块时的make -C M= modules的参数说明
    [LeetCode周赛复盘补] 第 第 90 场双周赛20221015
    生成对抗网络Generative Adversarial Network,GAN
    IOTE 2023盛况回顾,美格智能聚连接之力促数字新生长
    html页面仿word文档样式(vue页面也适用)
    前端Vue页面中如何展示本地图片
  • 原文地址:https://blog.csdn.net/weixin_53369402/article/details/128174471