又玩了快两期,一直在观望的小米笔记本第二代更新的有些失望,觉得可以考虑入手其他的本子了….好了开始学习orz
之前的第二篇内容很少,也就写了一些进程间通讯的方式以及一些了解,为了方便记录实验与以及补充关于信号的使用及知识,所以重新写了篇。
异常
异常分为四种:
还有一种是中断,原因是来自IO设备的信号,属于异步,返回行为是总是返回到下一条指令,比如输入ctrl+c,网络中一个包接收完毕,都会触发这样的中断。
陷阱:有意的异常,主要用途实在用于程序与内核之间提供一个像过程一样的接口,也就是系统调用,系统调用运行在内核模式中,执行特权指令,访问的是内核中的栈。
进程
异常是允许操作系统内核提供进程概念的基本构造块。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序运行所需的状态组成,而状态包括存放在程序中的代码和数据以及堆栈、通用目的寄存器的内容、程序寄存器、环境变量以及打开文件描述符的集合。
如下图的结构:
一个逻辑流的执行时间与另一个流重叠,成为并发流,多个流并发地执行的一般现象称为并发。
进程的状态可以分为三种:
1、运行
2、停止
3、终止
关于进程相关的一些接口,就不说了,在第二篇中有相关的内容。
、
信号
操作系统利用异常来支持进程上下文切换,信号,是一种更高层的软件形式的异常,它允许进程和内核中断其他进程,它通知进程系统中发生了一个某种类型的事件来达到中断。
常用的信号如下:
在c语言中,发送信号可以使用:
int kill(pid_t pid,int sig);
//pid:目的进程号,sig:信号,成功返回0,否则-1
unsigned int alarm(unsigned int secs);
//返回前一次闹钟剩余的秒数,若以前没设置则为0
//alarm函数会在secs秒后发送SIGALRM信号给调用进程,如果secs为0,则不会调度
//安排新的闹钟
接收信号:
所有的上下文切换都是通过调用某个异常处理器(exception handler)完成的,当进程p从内核模式切换到用户模式时(如系统调用返回或者是完成了一次上下文切换),内核会检查进程p的未被阻塞的待处理信号的集合 pnb 值:pnb = pending & ~blocked (pending待处理信号,blocked是设置阻塞的集合)
如果 pnb == 0,那么就把控制交给进程 p 的逻辑流中的下一条指令
如果 pnb != 0:
选择 pnb 中最小的非零位 k,并强制进程 p 接收信号 k
接收到信号之后,进程 p 会执行对应的动作
对 pnb 中所有的非零位进行这个操作
最后把控制交给进程 p 的逻辑流中的下一条指令
每个信号类型都有一个预定义的『默认动作』,可能是以下的情况:
终止进程
终止进程并 dump core
停止进程,收到 SIGCONT 信号之后重启
忽略信号
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)
//成功则返回指向前次处理程序的指针,否则返回SIG_ERR
如果handler是SIG_IGN,那么忽略类型为num的信号
如果是SIG_DFL,那么类型为signum的信号会恢复默认行为。
否则,handler就是用户自定义的函数,这个函数被称为信号处理程序 ,当信号处理程序被调用,则这一行为称为,捕获信号。
控制流如下图:
阻塞信号:
显式阻塞,就需要使用 sigprocmask 函数了,以及其他一些辅助函数(要注意的一点就是,在多进程中,子进程会继承父进程的阻塞集合):
int sigprocmask(int how,const sigset *set,sigset_t *oldset);
int sigemptyset(sigset *set)
int sigfillset(sigset *set)
int sigaddset (sigset *set,int signum)
int sigdelset (sigset *set,int signum)
其他的函数就不用怎么介绍了,主要说说sigprocmask,其行为取决于how的值,当how为:
SIG_BLOCK:把set中的信号添加到blocked(阻塞的信号集合)中,相当于blocked=blocked | set
SIG_UNBLOCK:从blocked删除set中的信号。
SIG_SETMASK:block=set
安全处理信号
信号处理器的设计并不简单,因为它们和主程序并行且共享相同的全局数据结构,尤其要注意因为并行访问可能导致的数据损坏的问题,这里提供一些基本的指南(后面的课程会详细介绍)
规则 1:信号处理器越简单越好
例如:设置一个全局的标记,并返回
规则 2:信号处理器中只调用异步且信号安全(async-signal-safe)的函数
诸如 printf, sprintf, malloc 和 exit 都是不安全的!
规则 3:在进入和退出的时候保存和恢复 errno
这样信号处理器就不会覆盖原有的 errno 值
规则 4:临时阻塞所有的信号以保证对于共享数据结构的访问
防止可能出现的数据损坏
规则 5:用 volatile 关键字声明全局变量
这样编译器就不会把它们保存在寄存器中,保证一致性
规则 6:用 volatile sig_atomic_t 来声明全局标识符(flag)
这样可以防止出现访问异常
这里提到的异步信号安全(async-signal-safety)指的是如下两类函数:
所有的变量都保存在栈帧中的函数
不会被信号中断的函数
Posix 标准指定了 117 个异步信号安全(async-signal-safe)的函数(可以通过 man 7 signal 查看)
正确的信号处理
信号并不是排队处理的,因为在pending位向量中,每种类型的信号只对应一位,所以每种类型最多只能有一个未处理的信号。
比如下面这段代码,发送5次同一个信号给主线程,那么会调用5次么?并不,在我的虚拟机上跑的结果是只跑了一次,而其余的信号都被扔了。
int val=0;
void func(){
val++;
sleep(1);
return;
}
int main(){
int i;
signal(SIGUSR2,func);
if(fork()==0){
for(i=0;i<5;i++){
kill(getppid(),SIGUSR2);
}
exit(0);
}
wait(NULL);
printf("val: %d",val);
return 0;
}
避免并发错误
如下代码,在向jobs列表中增加与删除工作的时候,看上去很安全,但是有一种可能的危险,当fork后,内核调度的是子进程运行,然后在父进程运行之前,子进程就结束了,此时就传SIGCHILD信号过去,也就是在父进程阻塞之前执行了信号处理函数,导致错误,当然以下程序我运行了好几遍,很小几率出现错误,但是实际工作肯定是不能容许这样的错误发生。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<apue.h>
#define SIZE 100
int jobs[SIZE]={0};
int currindex=0;
typedef __sigset_t sigset_t;
void addjob(pid_t tmp){
jobs[currindex]=tmp;
currindex++;
}
void deljob(pid_t tmp){
int i;
for(i=0;i<SIZE;i++){
if(jobs[i]==tmp){
jobs[i]=0;
return;
}
}
fprintf(stdout,"error! \n");
exit(0);
}
void func(){
pid_t pid;
sigset_t mask,pre;
sigfillset(&mask);
while((pid=waitpid(-1,NULL,0))>0){
sigprocmask(0,&mask,&pre);
deljob(pid);
sigprocmask(2,&pre,0);
fprintf(stdout,"del pid:%d \n",pid);
}
}
int main(){
pid_t pid;
signal(SIGCHLD,func);
sigset_t mask,pre;
sigfillset(&mask);
int i=0;
while(i<SIZE){
pid=fork();
if(pid==0){
execve("/root/Desktop/lerning/hello",NULL,NULL);
}
sigprocmask(0,&mask,&pre);
addjob(pid);
sigprocmask(2,&pre,0);
fprintf(stdout,"add pid:%d \n",pid);
i++;
}
exit(0);
}
既然可能在父进程之前结束发送信号,那么我们就在父进程的时候阻塞SIGCHILD,且因为子进程继承父进程的文件信息包括阻塞信号的集合,所以我们需要在子进程中恢复blocked即可。这样当子进程先结束,发送SIGCHILD信号的时候会被阻塞,然后执行完addjob才能恢复blocked,然后执行信号处理,然后就ok了~_~
int main(){
pid_t pid;
signal(SIGCHLD,func);
sigset_t mask,pre,one_mask,one_pre;
sigfillset(&mask);
sigemptyset(&one_mask);
int i=0;
while(i<SIZE){
sigprocmask(0,&one_mask,&one_pre);
pid=fork();
if(pid==0){
sigprocmask(2,&one_pre,0);
execve("/root/Desktop/lerning/hello",NULL,NULL);
}
sigprocmask(0,&mask,&pre);
addjob(pid);
sigprocmask(2,&pre,0);
fprintf(stdout,"add pid:%d \n",pid);
i++;
}
exit(0);
}
等待信号
当我们在程序中,需要在主进程中等待子进程运行结束,然后去做一些事,如:
volatile sig_atomic_t pid;
void handler(){
int olderrno=errno;
pid=wait(NULL);
errno=olderrno;
}
int main(){
sigset_t mask,pre;
signal(SIGCHILD,handler);
sigemptyset(&mask);
sigaddset(&mask,SIGCHILD);
while(1){
//阻塞SIG_CHILD,防止子进程结束过早,在初始化pid=0之前就发送SIG_CHILD信号
sigprocmask(SIG_BLOCK,&mask,&pre);
if(Fork()==0){
sigprocmask(SIG_SETMASK,&pre,NULL);
dosomething1();
exit(0);
}
pid=0;
sigprocmask(SIG_SETMASK,&pre,NULL);
while(!pid);
dosomething2();
}
return 0;
}
按照上面所写的代码,为了等待子进程结束再执行主线程的任务,使用一个死循环去等待,这样会很耗费内存。
我们可以使用sleep
让进程睡眠改成如下,但是这会让程序运行太慢。
while(!pid)
sleep(1);
而使用pause
似乎没什么问题,但可能会出现一种情况,当SIG_CHILD信号在while
之后pause
之前发出,那么主进程将会一直挂起或者睡眠。
while(!pid)
pause();
为了解决这样的问题,可以使用sigsuspend:
int sigsuspend(const sigset_t *mask);
//mask为暂时替换当前阻塞的集合,之后会还原为之前的集合
//该函数相当于在调用pause()函数前设置sigpromask函数进行阻塞,之后恢复blocked集合,如下:
//相当于的下面代码原子(不可中断)的版本。
//sigpromask(SIG_BLOCK,&mask,&pre)
//pause();
//sigpromask(SIG_SETMASK,&pre,NULL);
使用上面的函数,修改为如下,为了避免在进入while
之后立马接受到SIG_CHILD,就干脆直接把阻塞范围扩大到包括while
,然后使用sigsuspend
将阻塞集合替换为没有SIG_CHILD的集合,这样就确保了能在while当即使用了pause,又能在进程在pause后的状态下接受到SIG_CHILD信号,然后唤醒主线程。
int main(){
sigset_t mask,pre;
signal(SIGCHILD,handler);
sigemptyset(&mask);
sigaddset(&mask,SIGCHILD);
while(1){
sigprocmask(SIG_BLOCK,&mask,&pre);
if(Fork()==0){
sigprocmask(SIG_SETMASK,&pre,NULL);
dosomething1();
exit(0);
}
pid=0;
while(!pid)
sigsuspend(&pre);
sigprocmask(SIG_SETMASK,&pre,NULL);
dosomething2();
}
return 0;
}
实验:制作一个简单的shell
实验文件去csapp官网下,我做的是第二版的,在刚开始解压实验的时候,发现很多文件,有一些小慌,因为前阵子都是分析汇编代码,很久没怎么写代码了,所以这次实验和以前不一样,难度也是很大,需要冷静下来,一步一步分析shell的一些实现,以及lab中的文件。
以下是实验文件:
其中tracexx.txt为测试文件,sdriver.pl为测试程序,tshref是已经实现的shell ,tshref.out则是程序输出结果的保存文件,我截取输出结果的一段内容分析吧:
./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit
可以看到测试的方法为./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
,而以下的内容就是trace03.txt的内容,它会让的tshref这个shell中执行quit命令。
而我们要做的就是编写tsh.c,制作一个我们自己的shell文件,最好输出结果能和tshref的输出结果一样,所以我们可以参考测试文件trace中命令行对应的结果,去编写我们的shell。
下面我们来看看tsh.c的代码,分析我们要做什么,下面是main函数的编写,是一个编写多进程程序很好的例子,tsh中的代码可以对应《深入理解计算机系统》中P524的内容,也可以参考上面的去完成我们的实验,书上面缺少的是对后台进程使用完的回收,最好去看书了解,下面来看看main吧:
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */
/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);
/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
/* Install the signal handlers */
/* These are the ones you will need to implement */
signal(SIGINT, sigint_handler); /* ctrl-c */
signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
/* This one provides a clean way to kill the shell */
signal(SIGQUIT, sigquit_handler);
/* Initialize the job list */
initjobs(jobs);
/* Execute the shell's read/eval loop */
while (1) {
/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}
/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}
exit(0); /* control never reaches here */
}
可以看到main函数编写得让人看着舒服,注释应该说的很,可以直接从signal那开始看起。
程序在运行while就开始相当于运行shell了,开始一个死循环,然后调用eval
函数去处理命令行,而我们去看tsh.c文件,发现几个函数都是空着的,这就意味着,这次实验目的就是补充这空缺的函数。
/* Here are the functions that you will implement */
void eval(char *cmdline);//执行命令行
int builtin_cmd(char **argv); //解析命令行第一个参数是否是shell内置命令
void do_bgfg(char **argv); //执行bg与fg内置命令,一般为前台任务
void waitfg(pid_t pid); //等待前台任务完成
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
下面是tsh.c中的一些宏定义,与jobs的结构体。
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */
是不是看着很头大?因为上面代码包括很多内容,但是都有注释了,要耐心看下去,这些参数的作用。
那么,既然要一个shell,我们就要知道shell是怎么用的,如果熟悉Linux,没什么问题。
简单来说,shell 有两种执行模式:
如果用户输入的命令是内置命令,那么 shell 会直接在当前进程执行(例如 jobs)
如果用户输入的是一个可执行程序的路径,那么 shell 会 fork 出一个新进程,并且在这个子进程中执行该程序(例如 /bin/ls -l -d)
在这里,我们只要简单的实现shell的几个内置命令,bg,fg,jobs,quit。
job control:允许用户更改进程的前台/后台状态以及京城的状态(running, stopped, or terminated)
ctrl-c :会触发 SIGINT 信号并发送给每个前台进程,默认的动作是终止该进程
ctrl-z :会触发 SIGTSTP 信号并发送给每个前台进程,默认的动作是挂起该进程,直到再收到 SIGCONT 信号才继续
jobs :命令会列出正在执行和被挂起的后台任务
bg job :命令可以让一个被挂起的后台任务继续执行
fg job :命令可以让一个被挂起的前台任务继续执行
eval()函数实现:
eval的任务很简单,如下流程图:
代码:
void eval(char *cmdline)
{
char *argv[MAXLINE]; /*argument list of execve()*/
char buf[MAXLINE]; /*hold modified commend line*/
int bg; /*should the job run in bg or fg?*/
pid_t pid;
sigset_t mask; /*mask for signal*/
stpcpy(buf,cmdline);
bg = parseline(buf,argv);//parseline用于判断命令行最后一个参数是不是&
//是的话后台执行,不是则前台执行,以及整理参数格式
if(argv[0]==NULL){
return; /*ignore empty line*/
}
if(!builtin_cmd(argv)){ /*not a build in cmd*/
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigprocmask(SIG_BLOCK,&mask,NULL); /*block the SIGCHLD signal*/
if((pid = fork())==0)
{
sigprocmask(SIG_UNBLOCK,&mask,NULL); /*unblock the SIGCHLD signal in child*/
setpgid(0,0); /*puts the child in a new process group*/
if(execve(argv[0],argv,environ)<0){
printf("%s: Command not found\n",argv[0]);
exit(0);
}
}
addjob(jobs, pid, bg?BG:FG,cmdline); /*add job into jobs*/
sigprocmask(SIG_UNBLOCK,&mask,NULL); /*unblock the SIGCHLD signal in parent*/
if(bg){
printf("[%d] (%d) %s", pid2jid(pid), pid,cmdline);
}
else{
waitfg(pid); //等待前台任务执行结束
}
}
return;
}
这上面要注意的就是,之前说提到的并发问题,控制一个任务addjob与deljob的执行顺序,是在子进程产生一个job,然后放置job执行过快,在主进程addjob还没发生,子进程就结束发送信号调用deljob,所以用信号阻塞。
还有一个注意的地方就是,要把fork出的进程设置为新的进程组,否则在主进程中发生的中断或者收到的信号将被子进程收到。
builtin_cmd函数实现
这里就简单了,单纯的判断内置命令。
代码如下:
int builtin_cmd(char **argv)
{
/* not a builtin command */
if(!strcmp(argv[0],"quit"))
exit(0);
if(!strcmp(argv[0],"&"))
return 1;
if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg")){
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0],"jobs")){
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg函数实现
这里要注意的是,要知道bg、fg在shell中是如何使用的,以及使用的格式,从而判断参数格式是否正确,以及判断是fg或者bg决定是否后台运行。
代码:
void do_bgfg(char **argv)
{
pid_t pid;
struct job_t *job;
char *id = argv[1];
if(id==NULL){ /*bg or fg has the argument?*/
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return;
}
if(id[0]=='%'){ /*the argument is a job id*/
int jid = atoi(&id[1]);
job = getjobjid(jobs,jid);
if(job==NULL){
printf("%%%d: No such job\n",jid);
return;
}
}else if(isdigit(id[0])){ /*the argument is a pid is a digit number?*/
pid = atoi(id);
job = getjobpid(jobs,pid);
if(job==NULL){
printf("(%d): No such process\n",pid);
return ;
}
}else{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
kill(-(job->pid),SIGCONT); /*send the SIGCONT to the pid*/
if(!strcmp(argv[0],"bg")){ /*set job state ,do it in bg or fg*/
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid,job->cmdline);
}else{
job->state = FG;
waitfg(job->pid);
}
return;
}
waitfg函数实现
这里使用sleep(0)可以让cpu去调度其他进程,并非是真的要进程挂起0毫秒,这里也可以使用sigsuspend函数然后再该函数中的while前后阻塞SIG_CHILD信号,但是这样就要额外定义一个全局变量pid_t
,然后在SIG_CHILD
信号处理函数中pid_t=waitpid()
,如果pid!=pid_t
就继续waitfg中的while循环,等待前台执行完成才跳出循环。
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs)){
sleep(0);
}
return;
return;
}
sigchld_handler函数实现
按照测试文件输出编写的函数,输出进程状态,输出进程是正常暂停或者终止,还是因为收到信号而暂停,WNOHANG|WUNTRACED参数,该参数的作用是判断当前进程中是否存在已经停止或者终止的进程,如果存在则返回pid,不存在则立即返回。
代码:
void sigchld_handler(int sig)
{
pid_t pid;
int status;
while((pid = waitpid(-1,&status,WNOHANG|WUNTRACED))>0){
if(WIFEXITED(status)){ /*process is exited in normal way*/
deletejob(jobs,pid);
}
else if(WIFSIGNALED(status)){/*process is terminated by a signal*/
printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
deletejob(jobs,pid);
}
else if(WIFSTOPPED(status)){/*process is stop because of a signal*/
printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WSTOPSIG(status));
struct job_t *job = getjobpid(jobs,pid);
if(job !=NULL )job->state = ST;
}
}
if(errno != ECHILD)
unix_error("waitpid error");
return;
}
sigint_handler函数实现
因为我们为每个子进程设置为新的进程组,所以要想ctrl+c中断任务,就需要kill发送信号
代码:
void sigint_handler(int sig)
{
pid_t pid = fgpid(jobs);
if(pid != 0){
kill(-pid,SIGINT);
/*let the sigchld_handler to delete the job in jobs?*/
}
return;
}
sigtstp_handler函数实现
也很简单实现。
代码:
void sigtstp_handler(int sig)
{
pid_t pid = fgpid(jobs);
if(pid!=0 ){
struct job_t *job = getjobpid(jobs,pid);
if(job->state == ST){ /*already stop the job ,do‘t do it again*/
return;
}else{
kill(-pid,SIGTSTP);
}
}
return;
}
至此,该实验完成,其实这个实验让我们补充的部分虽然不多,但是却非常核心,包含了我前面写的解决并发问题使用信号,以及sleep(0)或者sigsuspend同步或者说等待信号,以及信号相关的函数,以及多进程的使用的知识点,而这也是初步了解进程的必要基础。
上面的代码并非完全是我写的,我也参考了其他的解析,然后看书理解写的,其实过程很简单,只是需要了解的知识相对较多罢了。