1、Unix程序设计基础 第二讲,上一讲回顾,80386 CPU介绍 实模式与保护模式 特权级别 分段与分页 系统调用原理 软中断引起特权级别的切换 int 80h 被封装成一组C函数,上一讲回顾,Unix下对文件与设备的操作 文件描述字 设备文件 文件操作系统调用 打开,创建,关闭文件 文件访问权限 读写文件 文件定位,操作系统的重要概念:进程,什么是进程? Unix下的进程 Unix下的多进程编程 进程控制 信号处理 进程间通信 特殊的进程:线程,什么是进程?,几个定义: APUE: An executing instance of a program is called a process.
2、 不准确:程序一次运行可以创建多个进程 实质上根本不对:在Unix下程序的运行并不产生一个新进程 我的定义:进程是具有独立地址空间的运行单位,“独立地址空间”很重要,Unix使用flat address,以32位系统为例,地址范围从0x0-0xffffffff。任何地址都是虚拟地址,要通过页面映射才能得到物理地址,这个过程对进程来说是透明的,进程看到的都是虚拟地址。 “独立地址空间”是指各个进程都有自己的虚拟地址空间(在Linux下为0x0-0xbfffffff),而且任何进程都只能访问到自已经的虚拟地址空间。,进程的并发性,宏观上,所有进程都是并发运行的。 微观上,除非是多处理器,否则不可能
3、有两个进程在同时运行。具体方法是时间片轮转:一个进程运行一个时间片,就把CPU让出来让另一个进程运行。因为时间片很小,所以用户看起来所有进程都在运行。 任何两个不相关的进程其推进速度可能是任意的。,并发带来的好处与挑战,很明显好处:可以让多个用户分享CPU。对单用户而言,也可同时运行多个程序,如一边上网一边QQ。 更深层次的好处:充分利用CPU资源。 当一个进程在等待数据时(从网络,外部设备等),其它进程可占用CPU。,并发带来的好处与挑战,挑战:并不是所有的事情都可以同时做。 两个进程同时写一个文件,对于普通文件,文件某一个位置上的内容是最后一次写入的结果。好像还不太糟。 但如果这个文件是一
4、台打印机那将会怎么样?可以想像打印出来的东西将不是任何一个进程想得到的。 数据的不一致性。,数据的不一致性,例:多个进程通过共享内存通信(一种进程间通信方式,与地址空间独立性无关),共享一块物理地址。每个进程都通过int *p映射到这块物理地址。进程每次获取一个网页,调用*p=*p1。最后*p的值就是多个进程获取到的网页总合。,数据的不一致性,进程1 mov eax, pinc eax mov p, eax,进程2mov eax, p inc eax mov p, eax,结果不是我们想要的,*p只被加了1!,数据的不一致性,因为*p是共享资源,因此对它写操作应该是互斥的。访问文件也是类似。
5、在编写多进程或多线程程序时应当特别注意。,Unix下的进程,五种基本状态 新建 进程正在被创建 就绪 进程正在等待被调度 运行 进程正占用CPU 睡眠(阻塞)进程正在等待一个事件,例如I/O 僵死 进程已经结束,正在等待释放资源$ ps guax 查看系统中的所有进程的详细情况,状态之间的转换,进程ID与进程间的关系,UNIX系统中所有进程都有一个唯一的,称为进程标识的正整数与之相联,称为进程ID,简称PID。 除了init进程(PID=1,所有进程的祖先),任一进程都有唯一的父进程。 若干进程可以属于一个进程组,进程组也有一个唯一进程组ID。,Unix下的多进程编程,分三个部分1、进程控制
6、2、信号处理 3、进程间通信(IPC),进程控制,进程创建fork函数原型: #include pid_t fork(void); UNIX下最优美的函数。 pid_t是一个unisigned int,是进程号对应的数据类型,进程创建,“fork”的意思就是一分为二,把当前进程复制出一个新的进程。当前的进程就是新进程的父进程,新进程称为子进程。fork把子进程ID返回给父进程,把0返回给子进程,通过对返回值的检查就可知道当前是父进程还是子进程。看看下面的例子就明白我在说什么。,#include #include #include int main(void) pid_t pid;if (pid
7、 = fork() 0)printf(“I am the parent, my pid = %u, my childs pid = %un”, getpid(), pid);else if (pid = 0)printf(“I am the child, my pid = %u, my parents pid = %un”,getpid(), getppid();elseperror(“fork”);return 1;return 0; ,进程创建,一般结构:if (pid = fork() 0)parents code;else if (pid = 0)childs code;elseerr
8、or handling; 父进程打开的文件描述字将被子进程继承。,进程创建,获得当前进程id: getpid获得父进程id: getppid 函数原型: #include pid_t getpid(void); pid_t getppid(void);,执行一个新程序,执行程序系统调用execve 函数原型: #include int execve(const char *path, const char *argv,const char *envp); Unix还提供其它几个执行程序函数,execl,execlp,execle,execv,execvp都不是系统调用,依赖于execve。,执
9、行一个新程序,path,执行的文件 argv,参数表 envp,环境变量表,一般直接用environ就行 如: char *argv = “gcc”, “-g”, “-c”, “rbtree.c”, NULL; execve(“/usr/bin/gcc”, argv, environ);,执行一个新程序,execve启动一个新的程序,新的地址空间完全覆盖当前进程的地址空间,但当前进程把开的文件描述字(除非特别设置),当前工作目录等将被继承。execve只返回负值表示调用失败,如果成功的话将永不返回。,shell执行程序的原理,敲入命令:$ psshell进程到底做了什么事?1、等待用户输入 (
10、等待I/O,睡眠状态)2、获得输入ps3、fork();子进程把自己放到前台,并调用execve(“ps”, );父进程把子进程放入前台,并等待子进程结束(父进程进入睡眠状态 )4、子进程结束,父进程得到子进程的结束状态信息,并把自己放到前台,回到1。,shell执行程序的原理,由此可以看出,进程被创建的原因是因为fork被调用,而execve只是把当前进前的地址空间替换成新程序的地址空间。因此,不能说“进程是程序的一次执行”,“程序的执行”只是地址空间的替换。 思考: 在3中为什么既要父进程把子进程放到前台,又要子进程把自己放到前台? 有兴趣的话可以自己编写一个shell。,等待进程完成,子
11、进程运行结束后(正常或异常),它并没有马上从系统的进程分配表中被删掉,而是进入僵死状态(Zombie),一直等到父进程来回收它的结束状态信息。 如果父进程没有回收走子进程的结束状态就已经退出,子进程将永远处于僵死状态;也有例外,如父进程先于子进程结束,子进程将被init进程继承,并回init进程回收其结束状态信息。,等待进程完成,回收子进程结束状态信息wait, waitpid函数原型: #include pid_t wait(int *stat_loc); pid_t waitpid(pid_t pid, int *stat_loc,int options);,等待进程完成,当进程调wait
12、,它将进入睡眠状直到有一个子进程结束。wait函数返回子进程的进程id,stat_loc中返回子进程的退出状态。waitpid的第一个参数pid的意义:pid 0: 等待进程id为pid的子进程。pid = 0: 等待与自己同组的任意子进程。pid = -1: 等待任意一个子进程pid -1: 等待进程组号为-pid的任意子进程。因此,wait(&stat)等价于waitpid(-1, &stat, 0),等待进程完成,waitpid第三个参数option可以是0,WNOHANG, WUNTRACED或它们的按位或(”|”)。WNOHANG表示不进入睡眠状态,即如果指定的子进程都还没有僵死掉,
13、立即返回0。WUNTRACED 我也不太清楚,不管它,多进程程序的一般结构,if (pid = fork() 0) parents code;wait(); else if (pid = 0)childs code; elseerror handling;return xxx;,信号处理,概念:信号是Unix操作系统用来通知进程发生了某种事件的一种手段。 Unix程序设计教程:信号也称为软中断。 注意与上节课中讲的软中断区分。 事件的种类包括: 程序错误类,程序中止类,闹钟类,I/O类, 作业控制类,操作错误类,其它 在/usr/include/asm/signal.h中列出所有的信号名(SI
14、Gxxx)。,信号处理,当进程接收到一个信号(可能是自己发出,也可能是别的有权限的进程发出),它可以采取的动作可以是下面任意一种: 忽略信号:SIGSTOP与SIGKILL除外。 捕获信号:当信号出现时调用专门提供的一个函数,这个函数称为信号号柄。SIGSTOP与SIGKILL除外。 执行信号的默认动作。,几个常见的信号,SIGINT: 前台程序执行过程中按下Ctrl-c就会向它发出SIGINT信号,默认动作终止进程。 SIGKILL: 立即中止进程,不能被捕获或忽略。 SIGTERM: kill命令默认的中止程序信号。 SIGQUIT: Ctrl-发出的信号,默认动作终止进程并生成core文
15、件。 SIGALRM: 定时器到期,可用alarm函数来设置定时器。默认动作终止进程。,几个常见的信号,SIGCHLD: 子进程终止或停止,默认动作为忽略。 SIGSTOP: 停止进程。不可忽略或捕获。 SIGCONT: 继续被停止进程。不可忽略。 SIGTSTP: Ctrl-z向程序发出的停止信号。 SIGUSR1、SIGUSR2: 程序可利用信号。默认动作终止进程。,设置信号动作系统调用,设置信号动作signal,sigaction。函数原型:#include void (*signal(int sig, void (*func)(int)(int);int sigaction(int s
16、ig, const struct sigaction *act, struct sigaction *oact);signal函数的声明有点复杂,解释一下怎么看这数据的定义与声明。,设置信号动作系统调用,从数据的名字开始,在这里是signal,向右看,是”(“,说明它是一个函数,括号内为参数列表;再向左看,是一个”*”,说明它的返回值是一个指针;再向右看,看到”(“,说明该指针指向一个函数,括号内为参数表。再向左看这个函数的返回值,是void。结束。基本方法就是右左右左再来一个复杂点的。,设置信号动作系统调用,typedef int (*(*(*fp)(void *)10) (int); 定义
17、fp为一个指针类型,指一个函数,其返参数表是(void *),返回一个指向数组的指针,数组有10个元素,元素类型是一个指针,指向一个函数,其参数列表为(int ),返回值是int。 Has a try: 声明一个数组,包括10个元素,每个元素是一个指针,指向一个函数,其参数列表为(int),返回值是指针,指向一个有10个元素的数组,元素类型是指针,指向另一个指针,这个指针指向fp型(上面定义好的那个)。,设置信号动作系统调用,继续讲signal函数。第一个参数sig是要设定动作的信号名,如SIGALRM,第二个参数func是接收到这个信号是要执行的动作。func可以是SIG_DEL,SIG_I
18、GN,分别表示默认动作与忽略,也可以是自定义的参数表(int),返回值int的函数。signal成功返回原来的信号处理函数,失败返回SIG_ERR。看下面例子。,#include #include static int flag = 0;static void sig_alrm(int signo) flag = 1; int main(void) if (signal(SIGALRM, sig_alrm) = SIG_ERR) perror(“signal”);exit(1);alarm(10);pause();if (flag)printf(“SIGALRM receivedn”);ret
19、urn 0; ,设置信号动作系统调用,上面的程序虽然很短但却引出来很多问题。先解释一下几个系统调用。设置定时器alarm,函数原型:#include unsigned int alarm(unsigned int seconds);在seconds秒后对本进程发送一个SIGALRM信号。一般情况下返回0;如果已经有一个定时器被设置且还没有到时间,否返回上一个定时器剩余的时间。,设置信号动作系统调用,示例中的另一个调用pause,以及一个相似的库函数sleep#include int pause(void);int sleep(unsigned int seconds);sleep让进程睡眠se
20、conds秒,pause让进程永远睡眠。如果在睡眠过程中被信号打断,它们将返回-1。,设置信号动作系统调用,例子中的两个语句:alarm(10);pause();的意义就是暂停10秒。10秒后pause()被SIGALRM打断,返回-1,进程继续运行。信号SIGALRM的handler把flags设为1并且返回,于是main知道是被SIGALRM信号中断,打印出”SIGALRM received”。,不可重入函数,为什么不在SIGALRM的handler(即函数sig_alrm)里调用printf?这就引出了不可重入函数的概念。不可重入函数是指这样的一类函数,不可以在它还没有返回就再次被调用。
21、例如printf,malloc,free等都是不可重入函数。因为信号可能在任何时候发生,例如在printf执行过程中,因此不能在信号处理函数里调用printf,否则printf将会被重入。,不可重入函数,函数不可重入大多数是因为在函数中引用了全局变量。例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。仔细体会不难发现这与前面说到多进程下的数据不一致性很相似,都是对共享数据的相斥访问问题。,这个例子是否存在问题?,alarm(10);pause();前面说到,进程的推进速度可能是任意的,如果执行完alarm,进程的时间片正好用完;过了十几秒之后才又轮到这个进程运行,会怎么样?结果是,pause在接收到信号之后才被调用。也就是说sig_alrm执行完返回pause()才被调用,结果是进程永久的睡眠(除非它再收到别的信号)。虽然这种情况几乎不可能发生,但我们还是应该避免这种潜在的错误。,下次课的内容,信号处理(续) 进程间通信(IPC) 线程的简单介绍 高级I/O 其它我认为有趣的Thanks!,