CS110操作系统原理学习笔记--系统调用(二)
主要涉及lecture4~lecture6, 系统调用的部分和多线程。
- 系统调用
- 多进程
- shell实现
- execvp
- pipe - signal
1.系统调用
每个进程都有自己的内存空间。each process operates as if it owns all of main memory.(virtual address space)。
进程内存布局:data->全局变量和静态变量; BBS: uninitialized data segment; Text: code; rodata: read-only data
SIGSEGV : 进入了不允许进入的内存或尝试对read-only的内存进行修改。
使用callq(q只是用来区别操作数的大小)会对一个函数指针解引用,包括用户态不可以解引用的存在kernel态的函数指针(比如read(opcode 0), write(opcode 1)...)。这些函数的参数会存在参数传递寄存器: %rdi %rsi %rdx %rcx %r8 %r9中,超6个就放栈上。然后系统会使用syscall发出一个中断(interrupt)(也可被叫做trap),然后促使中断处理器(interrupt handler)执行(切换成kernel态)。(stop my programer and handle this !)。
中断处理器会在kernel stack上建一个帧,然后执行相应代码,并把返回值放到%rax,然后执行返回指令iretq,然后切换回用户态。
详细的操作是,IDT(IDT的地址存在IDTR寄存器)里面的元素是一个个门描述符(gate descriptor),门描述符表里存着中断的信息和中断处理器的地址(interrupt handler address)。
相关的门描述符地址 = IDT base + offset
门描述符里存着segement selector和offset,用来找到相应代码的地址。(寻址方法https://zhuanlan.zhihu.com/p/69334474)
另一张图overview
summary : 对OS虚拟化,增加安全和可用性
2.多进程
- getpid() // unistd.h
- fork() // unistd.h
- waitpid() // sys/wait.h
getpid()查询当前进程pid
// file: getpidEx.c
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h> // getpid
int main(int argc, char **argv)
{
pid_t pid = getpid();
printf("My process id: %d\n",pid);
return 0;
}
fork创建一个新的进程,子进程中fork返回0,父进程fork返回子进程的pid,因此来区分进程。
int main(int argc, char *argv[]) {
printf("Greetings from process %d! (parent %d)\n", getpid(), getppid());
pid_t pid = fork();
assert(pid >= 0);
printf("Bye-bye from process %d! (parent %d)\n", getpid(), getppid());
return 0;
}
fork tree, 一共2222进程,一共打印1a, 2b, 4c, 8*d
static const char const *kTrail = "abcd";
int main(int argc, char *argv[]) {
size_t trailLength = strlen(kTrail);
for (size_t i = 0; i < trailLength; i++) {
printf("%c\n", kTrail[i]);
pid_t pid = fork();
assert(pid >= 0);
}
return 0;
}
返回
myth60$ ./fork-puzzle
a
b
b
c
d
c
d
c
d
d
c
d
myth60$ d
d
d
连续fork(), 1 + 1 + 2 + 4(2**3),一共8个进程
int main(void)
{
fork();
fork();
fork();
}
pid_t waitpid(pid_t pid, int *status, int options); Synchronizing Between Parent and Child
如果不关心子进程终止的信息,第一个参数为-1时表示任意child,第二个参数保存子进程结束的状态可以是NULL, 返回值子进程的pid(https://linux.die.net/man/2/waitpid)。可用宏命令来对status进行操作,比如WIFEXITED(status), 如果是true,则子进程正常结束。
pid_t pid = waitpid(-1, NULL, WNOHANG);
int main(int argc, char *argv[]) {
printf("Before.\n");
pid_t pid = fork();
printf("After.\n");
if (pid == 0) {
printf("I am the child, and the parent will wait up for me.\n");
return 110; // contrived exit status
} else {
int status;
waitpid(pid, &status, 0) // 如果不关心子进程终止的信息,第二个参数可以是NULL
if (WIFEXITED(status)) {
printf("Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Child terminated abnormally.\n");
}
return 0;
}
}
两种创建并同步多个子进程的方式,第一种无次序返回子进程
int main(int argc, char *argv[]) {
for (size_t i = 0; i < 8; i++) {
if (fork() == 0) exit(110 + i);
}
while (true) {
int status;
pid_t pid = waitpid(-1, &status, 0);
if (pid == -1) { assert(errno == ECHILD); break; } // ECHILD 无子进程了
if (WIFEXITED(status)) {
printf("Child %d exited: status %d\n", pid, WEXITSTATUS(status));
} else {
printf("Child %d exited abnormally.\n", pid);
}
}
return 0;
}
第二种按创建次序返回子进程
int main(int argc, char *argv[]) {
pid_t children[8];
for (size_t i = 0; i < 8; i++) {
if ((children[i] = fork()) == 0) exit(110 + i);
}
for (size_t i = 0; i < 8; i++) {
int status;
pid_t pid = waitpid(children[i], &status, 0);
assert(pid == children[i]);
assert(WIFEXITED(status) && (WEXITSTATUS(status) == (110 + i)));
printf("Child with pid %d accounted for (return status of %d).\n",
children[i], WEXITSTATUS(status));
}
return 0;
}
3.shell实现
通过execvp我们可以实现简单的指令运行,但是很多shell的功能还不能实现
execvp
int execvp(const char *path, char *argv[]);
使用fork时候大部分是像用exec,即让子进程运行另一个程序。
// 模拟shell
/**
* File: mysystem.c
* ----------------
* Implements our own system function (which I call so that it doesn't compete with
* the built-in system function).
*/
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/wait.h>
#include "string.h"
#include "exit-utils.h"
static const int kExecFailed = 1;
static int mysystem(const char *command) {
pid_t pid = fork();
if (pid == 0) {
// char* [], 传参数
char *arguments[] = {"/bin/sh", "-c", (char *) command, NULL};
execvp(arguments[0], arguments);
exitIf(true, kExecFailed, stderr, "execvp failed to invoke this: %s.\n", command);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
return WEXITSTATUS(status);
else
return -WTERMSIG(status);
}
static const size_t kMaxLine = 2048;
int main(int argc, char *argv[]) {
char command[kMaxLine];
while (true) {
printf("> ");
// char *fgets(char *str, int n, FILE *stream), 从stream中读取输入数据
fgets(command, kMaxLine, stdin);
if (feof(stdin)) break;
command[strlen(command) - 1] = '\0'; // overwrite '\n'
printf("retcode = %d\n", mysystem(command));
}
printf("\n");
return 0;
}
处理shell中的后台运行符&(ctrl+z 和 bg/fg)
bool isbg = strcmp(argv[count - 1], "&") == 0;
if (isbg) argv[--count] = NULL; // overwrite "&"
pid_t pid = forkProcess();
if (pid == 0) {
execvp(argv[0], argv);
printf("%s: Command not found\n", argv[0]);
exit(0);
}
if (isbg) {
printf("%d %s\n", pid, command); // 不使用waitpid来block父进程
} else {
waitpid(pid, NULL, 0); // block inline
}
4.pipe
int pipe(int pipefd[2]);
创建一个单方向的数据通道,可用于进程间的传递信息。
从fd[0]读,从fd[1]写
// https://cplayground.com/?p=narwhal-newt-cheetah
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int fds[2];
pipe(fds);
pid_t pid = fork();
// 子进程拷贝了父进程的pcb,拥有独立的文件描述符fd[0]和fd[1],不需要写时可以关闭fd[1](NB.openfile table是全局的,进程共享的,fd多了只是让他加了refcount)
// read没读到指定数量的信息(或遇到/0)前会阻塞,所以不必在乎是先写还是先读。
if (pid == 0){
close(fds[1]);
char buffer[6];
read(fds[0], buffer, sizeof(buffer));
printf("read from pipe bridging processes: %s.\n", buffer);
close(fds[0]);
return 0;
}
sleep(5);
close(fds[0]);
write(fds[1], "hello", 6); // hello + "\0"
waitpid(pid, NULL, 0);
close(fds[1]);
return 0;
}
5.dup2 重定向
include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup()创建一个文件描述符的拷贝(指向同个资源),新的文件描述符(返回值)是最低的未用的文件描述符(using the lowest-numbered unused file descriptor for the new descriptor)。dup2和dup做得一样的事情,但是所使用的新的文件描述符是newfd而不是任意低的未用描述符。返回-1为异常。
指向文件的文件描述符fd被复制到原来用于标准输出的描述符STDOUT_FILENO,则进程内系统函数向STDOUT_FILENO写入实质就是向fd写入,所有有了输出重定向。因为printf函数有一个缓存,要保证信息输出成功需要刷新缓存区。fflush(stdout)。
int fd = open(file_name, O_RDWR);
dup2(fd, STDOUT_FILENO);
printf("hello world\n");
fflush(stdout);
保存和恢复STDOUT_FILENO
int fd_stdout = dup(STDOUT_FILENO); // 把STDOUT_FILENO复制到fd_stdout,fd_stdout现在也指向标准输出了
// do sth
dup2(fd_stdout, STDOUT_FILENO); // 恢复STDOUT_FILENO,现在描述符STDOUT_FILENO又重新指向标准输出了
perror & dprintf
直接把STDIN_FILENO赋值到fds[0]是没法用的 stat failed: -: Bad file descriptor
https://www.unix.com/programming/58920-writing-stdin_fileno-file.html
/**
* File: subprocess.c
* ------------------
* Implements the subprocess routines, which is similar to popen and allows
* the parent process to spawn a child process, print to its standard output,
* and then suspend until the child process has finished.
*/
#include <unistd.h>
#include <sys/wait.h>
#include "exit-utils.h"
#include <string.h>
typedef struct subprocess_t {
pid_t pid;
int supplyfd;
} subprocess_t;
subprocess_t subprocess(const char *command) {
int fds[2];
pipe(fds);
subprocess_t process = { fork(), fds[1] }; // return process 所以可以往fds[1]写
if (process.pid == 0) { // child
close(fds[1]); // no writing
// 输入重定向, 标准输入的信息重定向到了fds[0], 关闭fds[0]不影响,现STDIN_FILENO指的位置open file entry就是fds[0]指的,所以键盘输入变为了文件输入。
dup2(fds[0], STDIN_FILENO);
close(fds[0]); // already duplicate
char *argv[] = {"/bin/sh", "-c", (char *) command, NULL};
execvp(argv[0], argv);
// child process cannot reach here
}
close(fds[0]);
return process;
}
int main(int argc, char *argv[]) {
subprocess_t sp = subprocess("/usr/bin/sort");
const char *words[] = {
"felicity", "umbrage", "susurration", "halcyon",
"pulchritude", "ablution", "somnolent", "indefatigable"
};
for (size_t i = 0; i < sizeof(words)/sizeof(words[0]); i++) {
dprintf(sp.supplyfd, "%s\n", words[i]); // 往文件描述符输出
}
close(sp.supplyfd);
int status;
pid_t pid = waitpid(sp.pid, &status, 0);
return pid == sp.pid && WIFEXITED(status) ? WEXITSTATUS(status) : -127;
}
信号
信号是一种提示进程一个事件的发生的方法。kernel能发出很多种信号,IGSEGV, SIGBUS, SIGINT...当尝试对一个空指针解引用时候,kernel会发出一个SIGSEGV信号(segmentation fault),如果没有提供一个自定义的signal handler来处理这个信号,SIGSEGV 会中止程序并生成一个core dump。进程间也能发送信号,比如SIGSTOP, SIGKILL。signal handler会在收到相应信号时执行。
在多进程编程中,有个信号很重要。当子进程改变状态(退出, crash, 停止,或者从停止中被唤醒),会往父进程发送一个SIGCHLD信号(没有signal handler的情况下,这个信号会被忽略)。
include <signal.h>
typedef void (*sighandler_t)(int); // sighandler_t-> 返回为void, 参数列表为int的handler的函数
sighandler_t signal(int signum, sighandler_t handler); // if(signal(SIGCHLD, reapChild) == SIG_ERR) return -1;
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
static const size_t kNumChildren = 5;
static size_t numDone = 0;
static void reapChild(int unused) {
waitpid(-1, NULL, 0);
numDone++;
}
int main(int argc, char *argv[]) {
printf("Let my five children play while I take a nap.\n");
signal(SIGCHLD, reapChild);
for (size_t kid = 1; kid <= 5; kid++) {
if (fork() == 0) {
// if it is child
sleep(3*kid); // sleep emulates "play" time
printf("Child #%zu tired... returns to dad.\n", kid);
return 0;
}
}
// code below is a continuation of that presented on the previous slide
while (numDone < kNumChildren) {
printf("At least one child still playing, so dad nods off.\n");
sleep(5);
printf("Dad wakes up! ");
}
printf("All children accounted for. Good job, dad!\n");
return 0;
}
返回
信号不会立刻被处理,可能会出现发了多次信号但是只有一个信号被处理的情况。signal的原理就是在kernel有一个bitmask,传输一个信号就是像指定的修改指定的bit位,而使用了handler后恢复那个bit位,所以很多个信号在处理前发送,将只能执行一次。
但是还是存在一个问题,如果把休眠时间从sleep(3*kid)改成sleep(3)会出现,多次signal一次调用handler的情况,这样就没法完全回收子进程 numDone 一直小于kNumChildren,程序死循环。
一个解决方法就是捕捉到signal后,多次调用waitpid直到没捕捉到结束的子进程,但这时候会有问题,waitpid没子进程结束会堵塞,父进程不能干其他的事情,性能浪费。把signal的第三个参数改为WNOHANG后,waitpid将不会阻塞等待子进程死亡。
static void reapChild(int unused) {
while (true) {
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid <= 0) break; // note the < is now a <=
numDone++;
}
}
关于WNOHANG和WUNTRACED
WNOHANG:回收已结束了的子进程,如果没有则返回设ERRNO成ECHILD且返回-1.
WUNTRACED: 对STOP状态的子进程有进行回收。
If you pass -1 and WNOHANG, waitpid() will check if any zombie-children exist. If yes, one of them is reaped and its exit status returned. If not, either 0 is returned (if unterminated children exist) or -1 is returned (if not) and ERRNO is set to ECHILD (No child processes). This is useful if you want to find out if any of your children recently died without having to wait for one of them to die. It's pretty useful in this regard.
The option WUNTRACED is documented as below, I have nothing to add to this description:
WUNTRACED The status of any child processes specified by pid that are stopped, and whose status has not yet been reported since they stopped, shall also be reported to the requesting process.
https://tinytracer.com/archives/c与汇编-段与寄存器/
https://stackoverflow.com/questions/33508997/waitpid-wnohang-wuntraced-how-do-i-use-these
https://stackoverflow.com/questions/22681021/waitpid-with-execl-used-in-child-returns-1-with-echild
https://zhuanlan.zhihu.com/p/7759839