异常就是控制流中的突变,用来响应处理器状态中的某些变化。处理器中的状态被编码为不同的位和信号,状态变化称为事件。处理器检测到事件发生时,就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序),处理程序完成后可能会返回到当前指令或下一条指令或终止程序。
系统中可能的 每种异常都分配了一个唯一的非负整数的异常号 。处理器检测到发生一个事件,并且确定了相应的异常号k,随后处理器触发异常,执行间接过程调用,通过异常表的表目k,转到相应的处理程序。异常号是异常表中的索引,异常表的起始地址存放在异常表基址寄存器里。
进程就是一个 执行中程序的实例 。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的,上下文是内核重新启动的一个被抢占的进程所需的状态。
进程提供给应用程序的关键抽象:一个独立的逻辑控制流和一个私有的地址空间。
使用getpid
函数可以获取进程ID,每个进程都有一个唯一的正数进程ID(PID)。
#include <sys/type.h> #include <unistd.h> pid_t getpid(); // 返回调用者PID,Linux系统中pid_t被定义为int pid_t getppid(); // 返回父进程的PID
终止进程一般有三种原因:
1. 收到默认行为是终止进程的信号
2. 从主程序返回
3. 调用exit
函数
#include <stdlib.h> void exit(int status); // 以status退出状态来终止进程
父进程通过fork
函数可以创建一个新的运行的子进程,该函数 被调用一次返回两次 ,在父进程中返回子进程PID,子进程中返回0。父子进程拥有相同但是独立的地址空间,每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值以及相同的代码,且子进程继承了父进程所有的打开文件。
新创建的子进程几乎与父进程完全相同,最大的区别是它们有不同的PID。父进程和子进程是并发运行的独立进程。
#include <sys/type.h> #include <unistd.h> pid_t fork(); // 子进程返回0,父进程返回子进程PID,出错返回-1
一个进程终止后,会保持在已终止状态,直到被它的父进程回收。父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,此时该进程就不存在了。一个终止但还未被回收的进程称为 僵死进程 ,僵死进程虽然没有运行,但是仍然消耗系统的内存资源。
如果一个父进程终止了,内核会安排init进程(PID为1,系统启动时由内核创建,是所有进程的祖先)成为它的孤儿进程的养父。进程通过使用waitpid
函数等待它的子进程终止或停止。
#include <sys/types.h> #include <sys/wait.h> // 成功时返回子进程的PID且该进程被系统回收,options取WNOHANG时返回0,其他错误返回-1 // 默认情况(options=0):waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止 // 等待集合中的一个进程如果在调用waitpid之前就已经终止,那么waitpid就立即返回 // pid>0时等待集合就是进程ID等于pid的子进程,pid=-1时等待集合由父进程的所有子进程组成 // options可设为常量WNOHANG,WUNTRACED和WCONTINUED的组合,可以修改waitpid的默认行为 // statusp参数非空时,waitpid会在status中放上关于导致返回子进程的状态信息 // 调用进程没有子进程时waitpid返回-1,且设置全局变量errno为ECHILD // 如果waitpid被一个信号中断时返回-1,且设置全局变量errno为EINTR // Unix函数的man页列出了使用该函数需要包含的头文件 pid_t waitpid(pid_t pid, int *statusp, int options); // waitpid的简单版本,wait(&status)等价于waitpid(-1, &status, 0) pid_t wait(int *statusp);
sleep
函数将一个进程挂起一段指定的时间,如果请求的时间到了sleep
函数返回0,否则返回还剩下的要休眠的秒数。sleep
函数可能会被信号中断而过早地返回。
pause
函数让调用进程休眠,直到该进程收到一个信号。
#include <unistd.h> unsigned int sleep(unsigned int secs); // 挂起进程secs秒,返回还要休眠的秒数 unsigned int pause(); // 让进程休眠,总是返回-1
execve
函数在当前进程的上下文中加载并运行一个新程序。
execve
函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
execve
调用一次且从不返回,只有当出现错误时(如找不到filename)才会返回-1。
argv变量指向一个以null结尾的指针数组,其中每个指针都是一个参数字符串,通常argv[0]是可执行目标文件名。
envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串形如name=value。
全局变量environ指向envp数组的第一个envp[0]。
#include <unistd.h> // 执行成功则不返回,错误则返回-1 int execve(const char *filename, const char *argv[], const char *envp[]);
int main(int argc, char **argv, char **envp);
getenv
函数可以有用来在环境变量数组中搜索字符串“name=value”,找到了则返回指向value的指针,否则返回NULL。
setenv
函数可以用newvalue代替oldvalue,但只有overwrite非零时才会这样,name不存在时相当于直接设置name。
如果环境变量数组中存在“name=value”的字符串,那么unsetenv
会删除它。
#include <stdlib.h> char *getenv(const char *name); // 存在则返回指向name的指针,否则返回NULL int setenv(const char *name, const char *newvalue, int overwrite); // 成功返回0, 错误返回-1 void unsetenv(const char *name); // 无返回
程序是一堆代码和数据,可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中。
进程是执行中程序的一个具体实例,程序总是运行在某个进程的上下文中。
fork
函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。
execve
函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,没有创建新进程,仍然有相同的PID,并继承了调用execve
函数时已打开的所有文件描述符。
#include "csapp.h" #define MAXARGS 128 /* Function prototypes */ void eval(char *cmdlines); int parseline(char *buf, char **argv); int builtin_command(char **argv); int main() { char cmdline[MAXLINE]; /* Command line */ while(1) { /* Read */ printf("> "); Fgets(cmdline, MAXLINE, stdin); if(feof(stdin)) exit(0); /* Evaluate */ eval(cmdline); } } void eval(char *cmdline) { char *argv[MAXARGS]; /* Argument list execve() */ char buf[MAXLINE]; /* Holds modified command line */ int bg; /* Should the job run in bg or fg? */ pid_t pid; /* Process id */ strcpy(buf, cmdline); bg = parseline(buf, argv); if(argv[0] == NULL) return; /* Ignore empty lines */ if(!builtin_command(argv)) { if((pid = Fork()) == 0) /* Child runs user job */ { if(execve(argv[0], argv, environ) < 0) { printf("%s: Command not found.\n", argv[0]); exit(0); } } /* Parent waits for foreground job to terminate */ if(!bg) { int status; if(waitpid(pid, &status, 0) < 0) unix_error("waitfg: waitpid error"); else printf("pid %d is terminated\n", pid); } else printf("%d %s\n", pid, cmdline); } return; } /* If first arg is a builtin command, run it and return true */ int builtin_command(char **argv) { if(!strcmp(argv[0], "quit")) /* quit command */ exit(0); if(!strcmp(argv[0], "&")) /* Ignore singleton*/ return 1; return 0; /* Not a builtin command */ } /* parseline - Parse the command line and build the argv array */ int parseline(char *buf, char **argv) { char *delim; /* Points to first space delimiter */ int argc; /* Number of args */ int bg; /* Background job? */ buf[strlen(buf)-1] = ' '; /* Replace trailing '\n' with space */ while(*buf && (*buf == ' ')) /* Ignore leading space */ ++buf; /* Build the argv list */ argc = 0; while((delim = strchr(buf, ' '))) { argv[argc++] = buf; *delim = '\0'; buf = delim + 1; while(*buf && (*buf == ' ')) /* Ignore space */ ++buf; } argv[argc] = NULL; if(argc == 0) /* Ignore blank line */ return 1; /* Should the job run in the background? */ if((bg = (*argv[argc-1] == '&')) != 0) argv[--argc] = NULL; return bg; }
Linux信号是一种更高层的软件形式的异常,允许进程和内核中断其他进程。一个信号就是一条消息,通知进程系统中发生了一个某种类型的事件。每种信号类型都对应于某种系统事件,低层的硬件异常是由内核异常处理程序处理的。
信号提供了一种机制,通知用户进程发生了这些异常。比如一些常见的Linux信号有:
一个发出而没有被接收的信号叫做待处理信号,任何时刻, 一种类型至多有一个待处理信号 。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队等待,只会被简单的丢弃。
一个进程可以选择性的阻塞接收某种信号,一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
一个待处理信号最多只能被接收一次,内核为每个进程在pending位向量中维护着待处理信号的集合,而在blocked位向量中维护着被阻塞的信号集合。
每个进程属于一个进程组,进程组是由一个正整数进程组ID来标识的。getgrp
函数返回当前进程的进程组ID。
#include <unistd.h> pid_t getgrp(); // 返回调用进程的进程组ID
默认一个子进程和它的父进程同属于一个进程组。一个进程可以通过使用setpgid
改变自己或其他进程的进程组,将进程pid的进程组改为pgid。
#include <unistd.h> // pid为0时,表示使用当前进程的pid;pgid为0时,就用pid指定的进程的PID作为进程组ID int setpgid(pid_t pid, pid_t pgid); // 成功则返回0,错误返回-1
/bin/kill
程序可以向另外的进程发送任意信号。linux> /bin/kill -9 15213
表示发送信号9(SIGKILL)给进程15213,一个负的PID会导致信号被发送到进程组PID中的每个进程。
Unix shell使用作业表示对一条命令行求值而创建的进程,任何时刻,至多只有一个前台作业和0或多个后台作业。
shell为每个作业创建一个独立的进程组,进程组ID通常取自作业中父进程中的一个。
键盘上输入Ctrl+C会导致内核发送SIGINT信号到前台进程组中的每个进程,结果是终止前台作业。
输入Ctrl+Z会发送一个SIGTSTP信号到前台进程组中的每个进程,结果是停止/挂起前台作业。
进程通过调用kill
函数发送信号给其他进程(包括自己)。
#include <sys/types.h> #include <signal.h> // pid大于0时,发送信号号码sig给进程pid // pid等于0时,发送信号sig给调用进程所在进程组中的每个进程,包括调用进程自己 // pid小于0时,发送信号sig给进程组|pid|中的每个进程 // 成功时返回0,错误则返回-1 int kill(pid_t pid, int sig);
进程通过alarm
函数向它自己发送SIGALRM信号。
#include <unistd.h> // 安排内核在secs秒后发送一个SIGALRM信号给调用进程,secs为0时不会调度安排新的闹钟 // 返回前一次闹钟剩余的秒数,以前如果没有设定闹钟,则返回0 unsigned int alarm(unsigned int secs);
当内核把进程p从内核模式切换到用户模式时,会检查检查p的未被阻塞的待处理信号集合。如果集合非空,内核会选择集合中的某个信号(通常是最小的k),并且强制p接收信号k,收到这个信号会触发进程采取某种行为。每个信号的默认行为是下面中的一种:
SIGKILL的默认行为是终止接收进程,SIGCHLD信号默认行为是忽略这个信号。进程可以通过signal
函数修改和信号相关联的默认行为,唯一的例外是SIGSTOP和SIGKILL(这两个信号的默认行为不可修改)。
handler可以取以下三种:
#include <signal.h> typedef void (*sighandler_t)(int); // 函数指针 // 成功时返回指向前次处理程序的指针,出错为SIG_ERR(不设置errno) sighandler_t signal(int signum, sighandler_t handler);
隐式阻塞机制:内核默认阻塞任何当前处理程序正在处理信号类型的待处理信号。
显示阻塞机制:使用sigprocmask
函数明确阻塞和解除阻塞选定的信号。
sigprocmask
函数改变当前阻塞的信号集合,具体行为取决于how的值:
oldset非空时,blocked位向量的就职会保存在oldset中。
#include <signal.h> // 成功返回0, 出错返回-1 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 初始化set为空集合 int sigemptyset(sigset_t *set); // 把所有信号都添加到set中 int sigfillset(sigset_t *set); // 把signum添加到set int sigaddset(sigset_t *set, int signum); // 从set中删除signum int sigdelset(sigset_t *set, int signum); //signum是set的成员则返回1,不是返回0,出错返回-1 int sigismember(const sigset_t *set, int signum);
// 临时阻塞接收一个信号 sigset_t mask, prev_mask; Sigemptyset(&mask); Sigaddset(&mask, SIGINT); // Block SIGINT and save previous blocked set Sigprocmask(SIG_BLOCK, &mask, &prev_mask); //... Code region that will not be interrupted by SIGINT // Restore previous blocked set Sigprocmask(SIG_SETMASK, &prev_mask, NULL);
安全的信号处理需要让程序能安全的并发运行。
volatile
声明全局变量,可以告诉编译器不缓存这个变量,强迫编译器每次在代码中使用该变量时都从内存读取。 sig_atomic_t
声明标志,处理程序可能会写全局标志来记录收到信号,主程序周期性读这个标志,相应信号,再清除该标志。C提供的类型sig_atomic_t
,对它读写保证会是原子的(不可中断)。volatile sig_atomic_t flag;
未处理的信号是不排队的,因此不可以使用信号来对其他进程中发生的事件计数,因为pending位向量中每种类型的信号只对应一位,所以每种类型最多只有一个未处理的信号。正确处理信号需要牢记一个关键思想: 如果存在一个未处理的信号就表明至少有一个信号到达了 。
Unix信号处理的另一个缺陷是不同的系统有不同的信号处理语义,比如signal
函数语义各有不同、系统调用可以被中断。解决这些问题,可以使用Posix标准定义的sigaction
函数,允许用户再设置信号处理时明确指定信号处理语义。但是sigaction
函数运用并不广泛,一个更简洁的方式是定义一个包装函数Signal
,它调用sigaction
。
Posix : Portable Operating System Interface,可移植操作系统接口
编写并发程序时,还需要以某种方式 同步流 ,保证不存在竞争导致的错误。
使用sigsuspend
函数可以用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,其行为要么是运行一个处理程序,要么是终止该进程。这个函数在显式等待某个信号处理程序运行时,比起循环等待不浪费资源,避免了引入pause()
带来的竞争,且比slepp()
更高效。
#include <signal.h> int sigsuspend(const sigset_t *mask); // 等价于下述代码的原子的版本(不可中断) // sigprocmask(SIG_SETMASK, &mask, &prev); // pause(); // sigprocmask(SIG_SETMASK, &prev, NULL);
ps
:列出当前系统中的进程(包括僵死进程)(Process Status)
top
:打印出关于当前进程资源使用的信息
/proc
:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容,比如cat /proc/loadavg
输出Linux系统当前的平均负载
a
--
123456789
更改id为3
--
test
更改id为2
--
commentor
伪造名称???
--
hhh
伪造名称???
--
yayay