CS162 shell

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

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

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

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

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

support for cd and pwd

chdir

#include<unistd.h>
int chdir(const char *path);
int fchdir(int fd);

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

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

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

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

getcwd

#include<unistd.h>
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
char *get_current_dir_name(void);

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

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

errno

#include<errno.h>

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

tokens

struct tokens {
    size_t tokens_length;
    char** tokens;
    size_t buffers_length;
    char** buffers;
}

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

函数指针cmd_fun_t

typedef int cmd_fun_t(struct tokens* tokens);

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

unused

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

getenv

#include <stdlib.h>

char *getenv(const char *name);

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);

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

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[]);

在任务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);

symbolic link

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

创建链接

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

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

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

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

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

ln -s /home/junhao junhao

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

删除链接

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

ls -l pathname

例子如下:

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

unlink linkname

stat

#include <sys/stat.h>

int stat(const char *restrict pathname,
         struct stat *restrict statbuf);

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

fork

#include <unistd.h>

pid_t fork(void);

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

wait

#include <sys/wait.h>

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. */

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

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

(gdb) set follow-fork-mode parent

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

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

process group

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

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

file descriptor

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

isatty

#include <unistd.h>

int isatty(int fd);

Path resolution

access

#include <unistd.h>

int access(const char *pathname, int mode);

检查当前结成是否可以访问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 <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

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

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

write与read类似。

dup

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

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

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

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

open

#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

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 <unistd.h>

int close(int fd);

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

linux文件类型

有如下3类文件:

uid

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

uid的介绍

Pipes

实现思路

pipe

#include <unistd.h>
int pipe(int pipefd[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 <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);

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

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

setpgid和setpgrp

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
pid_t sepgrp(void);

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

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

tcgetpgrp和tcsetpgrp

#include <unistd.h>
pid_t tcgetpgrp(int fd);
int tcsetpgrp(int fd, pid_t pgrp);

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

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

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

signal

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

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *restrict act,
              struct sigaction *restrict oldact);

该函数修改进程收到指定的某个信号(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);
};
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值