640 likes | 806 Vues
Unix 进程管理. 1. 进程的基本概念 2. 进程结构 3 . 进程映像 4. 进程状态 5. 进程调度 6. 进程控制 7. 进程通信 (IPC, INTER-PROCESS COMMUNICATION). 1. 进程的概念. 程序在其上下文中的一次执行 . 除 0 # 和 1 # 进程外 , 由 fork 创建的实体的集合. * 讲授以 UNIX System v 为例 , 为理解方便,有些篡改。. 2.UNIX 进程结构. 1 #. Login shell. shell. $ ls|sort. prog1. ls. pwd.
E N D
Unix 进程管理 1. 进程的基本概念 2. 进程结构 3 .进程映像 4. 进程状态 5. 进程调度 6.进程控制 7. 进程通信(IPC, INTER-PROCESS COMMUNICATION)
1.进程的概念 • 程序在其上下文中的一次执行. • 除0#和1#进程外,由fork创建的实体的集合 *讲授以UNIX System v为例,为理解方便,有些篡改。
2.UNIX进程结构 1# Login shell shell $ ls|sort prog1 ls pwd ls program1 sort program2 program3
3.UNIX进程上下文 • 进程上下文:指进程的用户地址空间内容、寄存器内容及与进程相关的核心数据结构;包括三部分"上下文":用户级、寄存器级、系统级 • 用户级上下文:正文段即代码(text);数据段(data);栈段(user stack):用户态执行时的过程调用;共享存储区(shared memory) • 把地址空间的段称为"区(region)":进程区表和系统区表(前者索引指向后者) • 系统上下文: • proc结构:总在内存,内容包括阻塞原因; • user结构:可以调出到外存,进程处于执行状态时才用得着,各种资源表格; • 进程区表:从虚拟地址到物理地址的映射; • 核心栈:核心态执行时的过程调用的栈结构;
进程PCB(proc + user) • Proc: struct proc{ }proc[N]; p_stat; 进程状态如:SRUN,SSLEEP,SZOMB... p_flag ; 每位代表一个含义,如:SLOAD,… p_pri ; 优先级,如:-100 - 127 p_cpu ; p_nice ;计算进程动态优先数 p_sig ;接受软中断信号 p_uid ;进程的用户标识 p_pid; 进程标识符 p_ppid; 父进程的标识符 p_time ;进程在内存或外存的驻留时间 p_addr ;u区的地址(现代 :p_ubptb,p_regin) p_size; u区的大小 p_ttyp;进程相关终端 p_textp ; 指向共享正文段表 p_wchan; 进程等待原因
进程PCB(proc + user) u_procp;(指向proc) u_tsize; u_dsize; u_ssize;(正文段,数据段,用户栈大小) u_rsav[2] ; u_qsav[2]; u_ssav[2];(保留进程现场) u_ttyp;(终端) u_cdir; (当前目录) u_signal[NSIG];(软中断处理程序) u_uid; u_gid; u_ruid; u_rgid;(用户,组标识) u_arg[6];(传递系统调用参数) u_ofile[NFILE];(用户打开文件表) u_base; u_count; u_offset;(文件系统调用) u_utime; u_stime; u_cutime; u_cstime;(CPU时间) • User : struct user {…} ;主要包括:
系统共享正文段表 text • Struct text{….} text[M]; x_caddr; ( 内存始地址) x_daddr;(磁盘始地址) x_size;(正文段大小) x_count; (共享进程计数)
进程上下文之间的联系 proc[N] text[ ] p_textp x_caddr p_addr 核心区 用户区 用户栈 数据段 核心栈 user 正文段 u_procp
4. 进程状态 • 进程有9个状态,由p_stat 和p_flag及处理机状态字决定: • 创建SIDL • 核心态运行SRUN • 用户态运行SRUN • 内存就绪SRUN&SLOAD • 外存就绪SRUN&!SLOAD • 内存睡眠 SSLEEP&SLOAD(SWAIT or SSTOP) • 外存睡眠SSLEEP&!SLOAD(SWAIT or SSTOP) • 可抢先SRUN • 僵死SZOMB 1 2 3 4 5 6 7 8 9
进程状态(续) 注意:状态“被抢先”与“内存就绪”的地位相同,要等到下一次进程调度时,才能回到“用户态执行”。 3 8 1 5 4 9 2 7 6
5. 进程调度 • Swtch:采用动态优先数法 p_pri=p_cpu/2 + PUSER + p_nice +NZERO 25 20 系统进程优先级高:SCHED=-100,打印机=10,I/O缓存=-50 .计算方法: 1.每个时钟周期,当前进程 p_cpu+1; 2.每秒钟时钟中断时,全部进程的 p_cpu/2,计算所有进程的 p_pri ,若当前进程不是最小,设runrun=1 3.进程从中断或陷入返回时,计算 当前进程的 p_pri 基本上每个进程占用CPU时间不会超过一秒
进程调度(续) • 调度时机 1. 进程结束。 2. 进程睡眠SSLEEP or SWAIT。 3.进程暂停 SSTOP。 4. 当前进程需扩充内存,内存没空,被交换到外存。 5. 等待共享段调入内存 6.进程从核心态返回时,runrun=1 当前进程状态如 何变换? .唤醒进程时可能设runrun .计算进程p_pri时可能设runrun
Swtch流程图 当前进程现场-->核心栈 栈指针--〉u_rsav[2] 在proc中搜索SRUN&SLOAD&p_pri最小的进程P 从P的u_rsav[2]-->栈指针寄存器 根据栈指针寄存器恢复P现场 ?
6. 进程控制 • fork:创建子进程 • wait:父进程等待子进程结束 • exit:进程结束 • exec:进程更换正文段
fork---创建进程 1。调用格式 { 0---子进程返回 子进程pid---父进程返回 fork( )= 例: main() {int x; while((x=fork())== -1); if(x==0) printf(“a”); else printf(“b”); printf(“c”); } 2。使用方法 main() { …. while((x=fork())== -1); if(x==0) { 子进程语句} else {父进程语句} 父、子都执行语句; } 结果 ? abcc? bcac? abcc? acbc? cabc?
fork() 内存有空? n 在proc 数组中找一个空表项 找到否? 在磁盘复制 y y 为子进程申请空间; 复制user,数据段; u_procp指向proc; u_utime=0; u_stime=0; 栈指针-->u_rsav[2]; p_pid=系统赋标识号码 p_stat=SRUN; p_ppid=当前进程pid; 复制父进程的p_textp,p_size p_uid,p_ttyp,p_nice... 子进程返回0; 父进程返回子进程pid; x_count++; f_count++ 子:0 父: 子pid
父进程创建子进程 proc[N] text[ ] p_textp 父 x_caddr p_textp 子 p_addr p_addr 核心区 用户区 用户栈 数据段 核心栈 user 用户栈 数据段 核心栈 user 正文段 u_procp u_procp
fork例 1.fork error 2 . i=5 i=10 i=7 3. i=7 i=5 i=10 4. i=5 i=7 i=10 main() { int child, i=2; if((child=fork())==-1) {printf("fork error. ");exit();} if(child==0) {i=i+3; printf(“i=%d\n”,i); } i=i+5; printf(“i=%d\n”,i); } 插入else呢?
子1 父 子1 子2 父 子2 main( ) { if(fork()==0) { 子1的代码段; if(fork()==0) {子2的代码段} else { 子1的代码段} } else {父代码段} } main( ) { if(fork()==0) { 子1的代码段} else {if(fork()==0) {子2的代码段} else {父代码段} } } 去掉这个else 谁执行这一段?
exec-执行文件 • 更换进程执行代码,更换正文段,数据段 • 调用格式:exec (文件名,参数表,环境变量表) • 例:execlp(“max”,15,18,10,0); execvp(“max”,argp) main() { if(fork()==0) {printf(“a”); execlp(“file1”,0); printf(“b”); } printf(“c”); } file1: main() { printf(“d”); } acd? cad? adc? abdc? adbcc?
exec(文件名,参数表) 参数复制到系统空间 取文件i节点,验证文件 释放老分区 按文件头指定的文件大小, 分配新分区,装入新分区 Exec参数复制到新用户栈 初始化各分区 使能返回用户态 释放文件i节点
text 正文段 可执行文件 proc 头 正文段 数据段 工作区 数据段 核心栈 user 数据段 核心栈 user
wait--等待子进程结束exit---终止进程 • 使用方法: main( ) { int n; …. if(fork()==0) {printf(“a”); exit(0); } wait(&n); printf(“b”); } printf(“c”);
exit(int status) 释放user,所有栈,数据段 关闭打开文件表 工作目录。 u_ofile,u_cdir p_stat=SZOMB 将所有子进程的父进程 改为1号进程(p_ppid) 释放正文段 x_count--; ... wakeup(p_ppid); status-->user user-->磁盘 p_addr-->盘user swtch
wait(int * stat) 从盘上取出子进程user 有子进程结束? (father) ( sun) u_cutime+=u_utime; u_cstime+=u_stime; u_cutime+=u_cutime; u_cstime+=u_cstime sleep(u_procp,PWAIT) user中的status-->stat 释放子进程的proc 返回子进程的pid
shell程序 …… while(true) { printf(“$”); scanf(“% s”,&command); 检查命令的语法; if (fork()==0) {….. execlp(command,参数表,0); } if (command后缀!=‘&’) wait( …); ….. }
上机作业 • getpid()---获取进程的pid 作业一: 每个进程都执行自己独立的程序,打印自己的pid,父进程打印两个子进程的pid; 父 子1 子2 子 1 父 作业二: 同作业一要求。子2与父代码同 子2 作业三:写一个命令处理程序,能处理max(m,n), min(m,n) average(m,n,l)这几个命令(前后台都可以)。
实例:UNIX_wait 演示子进程与父进程的关系和fork、exec、wait的使用; 程序main.c 功能是进行10次循环,创建2个子进程。循环到第3次时,等待子进程结束。 #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <sys/wait.h> pid_t wait(int *stat_loc); void perror(const char *s); #include <errno.h> int errno; int global;
main() { int local,i; pid_t child; if ((child=fork()) == -1) { // 创建失败 printf("Fork Error.\n"); } if (child == 0) {// 子进程 printf("Now it is in child process.\n"); if (execl("/home/xyong/work/ttt","ttt",NULL) == -1) { // 加载程序失败 perror("Error in child process"); } global=local + 2; exit(); }
// 父进程 printf("Now it is in parent process.\n"); for (i=0; i<10; i++) { sleep(2); printf("Parent:%d\n",i); if (i==2) { if ((child=fork()) == -1) { // 创建失败 printf("Fork Error.\n"); } if (child == 0) {// 子进程 printf("Now it is in child process.\n"); if (execl("/home/xyong/work/ttt","ttt",NULL) == -1) { // 加载程序失败 perror("Error in child process"); } global=local + 2; exit(); } }
if (i==3) { pid_t temp; temp=wait(NULL); printf("Child process ID: %d\n", temp); } } global=local + 1; exit(); }
程序test.c #include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void); int global; main() { int local; int i; pid_t CurrentProcessID, ParentProcessID; CurrentProcessID=getpid(); ParentProcessID=getppid(); printf("Now it is in the program TEST.\n"); for (i=0; i<10; i++) { sleep(2); printf("Parent: %d, Current: %d, Nunber:%d\n",ParentProcessID, CurrentProcessID,i); } global=local + 1; exit(); } 功能是进行10次循环。
7. 进程间通信(IPC, INTER-PROCESS COMMUNICATION) • U7.1 进程间通信的类型 • U7.2 信号(signal) • U7.3 共享存储区(shared memory) • U7.4 消息(message) • U7.5 信号量 • U7.6 管道(pipe) • U7.7 套接字(socket) 返回
U7.1 进程间通信的类型 • 低级通信和高级通信 • 低级通信:只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量和管程机制。优点的速度快。缺点是: • 传送信息量小:效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。 • 编程复杂:用户直接实现通信的细节,编程复杂,容易出错。 • 高级通信:能够传送任意数量的数据,包括三类:共享存储区、管道、消息。 返回
直接通信和间接通信 • 直接通信:信息直接传递给接收方,如管道。 • 在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址; • 在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址。 • 间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如消息队列。通常收方和发方的数目可以是任意的。
U7.2 信号(signal) 信号相当于给进程的“软件”中断;进程可发送信号,指定信号处理例程;它是单向和异步的。 返回
1.信号类型 • 一个进程向另一个进程或进程组(或自己)发送信号(kill系统调用),信号送到p_sig中。:发送者必须具有接收者同样的有效用户ID,或者发送者是超级用户身份 • 某些键盘按键,如:中断字符(通常是Ctrl+C或Del)、暂停字符(如Ctrl+Z) • 硬件条件,如:除数为零、浮点运算错、访问非法地址等异常条件 • 软件条件,如:Socket中有加急数据到达
2.对信号的处理 • 进程可以设置信号处理例程(signal系统调用),在接收到信号时就被调用,称为“捕获”该信号。信号处理例程的参数是接收到信号的编号。(u_signal[20]) • 进程也可以忽略指定的信号(SIG_IGN)。 • 只有SIGKILL信号(无条件终止进程)和SIGSTOP(使进程暂停)不能被忽略。 • 在库函数system()的实现中,通过fork和exec加载新程序之后,在父进程中对SIGINT和SIGQUIT都要忽略,然后wait直到子进程终止,才恢复对SIGINT和SIGQUIT的原有处理例程。 • 进程创建后为信号设立了默认处理例程(SIG_DFL),如:终止并留映象文件(core)
信号例 Q.user Q.proc 接受进程Q: main() { signal(n,f()); … interrupt … f() { … } … } 发送进程P: main() { … kill(pid,n); … … f() p_sig n u_signal[n] 中断处理 程序
U7.3 共享存储区(shared memory) 相当于内存,可以任意读写和使用任意数据结构(当然,对指针要注意),需要进程互斥和同步的辅助来确保数据一致性 • 创建或打开共享存储区(shmget):依据用户给出的整数值key,创建新区或打开现有区,返回一个共享存储区ID。 • 连接共享存储区(shmat):连接共享存储区到本进程的地址空间,可以指定虚拟地址或由系统分配,返回共享存储区首地址。父进程已连接的共享存储区可被fork创建的子进程继承。 • 拆除共享存储区连接(shmdt):拆除共享存储区与本进程地址空间的连接。 • 共享存储区控制(shmctl):对共享存储区进行控制。如:共享存储区的删除需要显式调用shmctl(shmid, IPC_RMID, 0); 1. UNIX的共享存储区系统调用
共享存储区系统调用 段标识shmid -1 错误 • int shmget(key,nbyte,flag)={ • char * shmat(shmid,addr,flag)=段虚地址shmaddr • int shmdt(shmaddr) • int shmctl(shmid,cmd,sbuf) 例: #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define KEY 75 #define k 1024
生产者进程 消费者进程 main() {int shmid;*p,i; char * shmaddr; shmid=shmget(KEY,K, 0777|IPC_CREAT); shmaddr=shmat(shmid,0,0); p=(int *) shmaddr; for(i=0;i<K;i++) *p++ = i*i; shmdt(shmid); shmctl(shmid,IPC_RMID,0); ….... } main() {int mid;*q,*t,i; char * maddr; mid=shmget(KEY,K, 0777); maddr=shmat(mid,0,0); q=(int *) maddr; for(i=0;i<K;i++) printf(“%d”,*q++); shmdt(mid); …..... } 是否有问题? t=(int*)shmat(mid,0,0);
U7.4 消息(message) 与窗口系统中的“消息”不同。通常是不定长数据块。消息的发送不需要接收方准备好,随时可发送。 返回
UNIX消息 • 消息队列(message queue):每个message不定长,由类型(type)和正文(text)组成 • UNIX消息队列API: • msgget依据用户给出的整数值key,创建新消息队列或打开现有消息队列,返回一个消息队列ID; • msgsnd发送消息; • msgrcv接收消息,可以指定消息类型;没有消息时,返回-1; • msgctl对消息队列进行控制,如删除消息队列; • 通过指定多种消息类型,可以在一个消息队列中建立多个虚拟信道 • 注意:消息队列不随创建它的进程的终止而自动撤销,必须用msgctl(msgqid, IPC_RMID, 0)。另外,msgget获得消息队列ID之后,fork创建子进程,在子进程中能否继承该消息队列ID而不必再一次msgget。
消息的系统调用格式及使用 • Message define: struct msgbuf{ long mtype; char mtext[N]; } • int msgget(key,flag)={msgid,-1); • int msgsnd(msgid,msgbuf,nbyte,flag); • int msgrcv(msgid,buf,nbyte,flag); • int msgctl(msgid,cmd,sbuf); cmd={IPC_STAT,IPC_RMID,IPC_SET) wait? wait?
服务器进程 客户进程 main() {int msgid,p,i; struct msgbuf msg; msgid=msgget(KEY, 0777|IPC_CREAT); for(;;){ msgrcv(msgid,&msg,256,1,0); p=(int*)msg.mtext; msg.mtype=*p; msg.mtext=“OK”; msgsnd(msgid,&msg,2,0);} ….. } main() {int mid;*q,*t; struct msgbuf m; mid=msgget(KEY, 0777); t=getpid(); m.mtype=1; q=(int*)m.mtext; *q=t; msgsnd(mid,&m,sizeof(int),0); …..… msgrcv(mid,&m,256,t,0); printf(“%s”,m.mtext); …. } for(i=0;i<=20;i++) signal(i,clearnup); cleanup() {msgctl(shmid,IPC_RMID,0); exit();} } ....
U7.5 Unix 信号量(Semaphore) • 用于进程之间的同步和互斥 • 实现信号量集机制,即一次可同时对多个信号量做P,V操作,减少死锁发生的概率。
信号量APIs • struct sem{ushort semval;---信号量值 • ushort sempid;-- 最后操作信号量的pid • ushort semncnt;--等待信号量值增加的进程个数 • ushort semzcnt;--等待信号量值为零的进程个数 • } • ●struct sembuf{ushort sem_num;信号量序号 ushort sem_op;信号量操作 ushort sem_flag; 访问标志 } 1.semget(key,count,flag)={semid,-1} 2.semop(semid,oplist,count) (struct sembuf (*oplist)[ ]) 3.semctl(semid,snum,cmd,arg) cmd={SETALL,GETALL,IPC_RMID)
信号量APIs(cont.) 1.sem_op>0 semval+sem_op,唤醒所有等待该值增加的进程 2. sem_op=0 semval=0,继续后面操作,否则semzcnt+1,进程睡眠 3.sem_op<0 .|sem_op|<semval semval+sem_op .|sem_op|>semval semncnt+1,进程睡眠 .|sem_op|=semval semval=0,唤醒所有等待该值为零的进程 。信号量值始终 ≥0 。两个等待队列---信号量值增加、信号量值为零 。一旦进程睡眠,唤醒后重新开始sem_op操作
用unix信号量实现P、V操作producer--concumer • sid=semget(KEY,3,0777|IPC_CREAT) • P(mutex)=>struct sembuf Wm={0,-1,SEM_UNDO}; semop(sid,Wm,1); • V(mutex)=>struct sembuf SIGm={0,1,SEM_UNDO}; semop(sid,SIGm,1); • P(s1)=>struct sembuf W1={1,-1,SEM_UNDO}; semop(sid,W1,1); • V(s1)=>struct sembuf SIG1={1,1,SEM_UNDO}; semop(sid,SIG1,1); • P(s2)=>struct sembuf W2={2,-1,SEM_UNDO}; semop(sid,W2,1); • V(s2)=>struct sembuf SIG2={2,1,SEM_UNDO} ; semop(sid,SIG2,1);