本文记录我在做shell这个作业时用到有关资源,如Linux系统调用、Linux基础知识、C语言知识等。
这里只是非常简略地记录了一下,并且可能有理解不正确的地方,你可以把本文当作一个索引和没有思路时的启发,详细的信息可以再去查,我也给出了一些可能会用到的文档链接。
另一方面,我不想这篇文章干扰了你的思路,如果你发现你完全没有用我说到的东西就实现了功能,那也不要怀疑自己,毕竟,shell肯定有多种实现的方法。
判分问题:我在判分的时候一直是8分,直到完成了signal handling后才变成了9分,所以不要担心分数为什么一直不变。
对于各个系统调用,推荐你看官方的Linux manual或者可以在命令行直接用man查看。
文章目录
- support for cd and pwd
- chdir
- getcwd
- errno
- tokens
- 函数指针cmd_fun_t
- unused
- getenv
- program execution
- C语言的 ...(ellipse)语法
- exec
- symbolic link
- 创建链接
- 删除链接
- stat
- fork
- wait
- exit vs kill
- gdb debug在父子进程间切换
- 在shell中启动另一个shell产生SIGTTIN报错
- process group
- file descriptor
- isatty
- Path resolution
- access
- strtok和strdup
- Redirection
- IO Redrection
- read write
- dup
- open
- close
- linux文件类型
- uid
- Pipes
- pipe
- 优先级问题
- 关闭不用的文件描述符
- Signal handling
- getpgid和getpgrp
- setpgid和setpgrp
- tcgetpgrp和tcsetpgrp
- signal
- sigaction
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
返回(如果有)。
getcwd
:如果路径长度超过了size
,则返回NULL
,并且errno
设置为ERANGE
,程序应该检查该错误,若需要,可以分配一个更大的buf
。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
族的函数将当前进程替换为pathname
或file
指定的进程,其中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族的函数用于等待子进程状态的改变,并且获取其信息,状态改变有以下几种情况:
- 子进程终止
- 子进程暂停
- 子进程恢复运行
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
中设置检查的方式,其值可以为:
- F_OK:检查文件是否存在。
- 由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_CREAT
和O_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。
优先级问题
这里有一个redirection
和pipes
的优先级问题,这可能是个有用的链接:pipe-redirection-precedence。下图也大致说明了二者的优先级。
关闭不用的文件描述符
这个非常重要,如果不关闭,子进程可能陷入一直等待输入的状态。如果你发现子进程无法终止,很可能是这个问题。
具体地,你可以看:
- 香港中文大学的小实验的2.2节
- CS 162的Section 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)时所执行的行为。其中:
signum
为指定的信号,可以是除了SIGKILL
和SIGSTOP
外的所有有效信号。act
中用于指定新的动作,不为NULL
,否则无法指定新动作。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);
};