linux 中的进程

71
1 Linux Linux 中中中中 中中中中

Upload: upton-suarez

Post on 03-Jan-2016

134 views

Category:

Documents


0 download

DESCRIPTION

Linux 中的进程. ?问题 ?. 计算机中什么时候开始有进程的? 计算机中的第一个进程是谁? 用户的第一个进程是谁? 所有的进程间有什么联系? 亲属、同步. 主要内容. 1. linux 系统进程启动过程. 2. 3. linux 下的用户进程编程. linux 信号量操作. 一、 linux 系统进程启动过程 ( 了解 ). 开机 系统启动(系统进程初始化) 用户登陆(用户进程运行). BIOS. 1. 计算机出厂后已有的东西. 两个重要芯片,一个空白硬盘 1 ) BIOS ( Basic Input / Output System ) - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Linux 中的进程

1

LinuxLinux 中的进程中的进程LinuxLinux 中的进程中的进程

Page 2: Linux 中的进程

2

?问题 ?• 计算机中什么时候开始有进程的?• 计算机中的第一个进程是谁?• 用户的第一个进程是谁?• 所有的进程间有什么联系?

– 亲属、同步

Page 3: Linux 中的进程

3

主要内容

linux 系统进程启动过程1

linux 下的用户进程编程2

linux 信号量操作3

Page 4: Linux 中的进程

4

一、 一、 linuxlinux 系统进程启动过程系统进程启动过程 (( 了了解解 ))一、 一、 linuxlinux 系统进程启动过程系统进程启动过程 (( 了了解解 ))

• 开机1. 系统启动(系统进程初始化)2. 用户登陆(用户进程运行)

BIOS

Page 5: Linux 中的进程

5

1. 计算机出厂后已有的东西两个重要芯片,一个空白硬盘

1) BIOS( Basic Input/ Output System) 一组程序(保存着计算机最重要的基本输入输出的程序、系统设置程序、开机后自检程序和系统自启动程序。)固化到计算机内主板上一个 ROM 芯片。

2) CMOS :系统配置参数(计算机基本启动信息,如日期、时间、启动设置等) 保存在主板上一块可读写的 RAM 芯片。

生活中常将 BIOS 设置和 CMOS 设置混说,实际上都是指修改 CMOS 中存放的参数。正规的说法应该是“通过 BIOS 设置程序对 CMOS 参数进行设置”。

Page 6: Linux 中的进程

6

2. 安装操作系统到硬盘系统安装过程会规划硬盘(分区),写入数据(系

统启动程序写入 MBR ,操作系统程序写入主分区)。

MBR

DPT

OS

主引导扇区:位于整个硬盘的 0 磁头 0 柱面 1 扇区,共 512 字节,包括:

① 硬盘主引导记录 MBR ( Master Boot Record ) 446 字节。检查分区表是否正确以及确定哪个分区为引导分区,并在程序结束时把该分区的启动程序(也就是操作系统引导扇区)调入内存加以执行。②硬盘分区表 DPT ( Disk Partition Table ) 64 字节。一共64 字节,按每 16 个字节 作为一个分区表项,它最多只能容纳4 个分区, DPT 里进行说明的分区称为主分区。

+ 结束标志 “ 55 , AA” ( 2 字节)

主引导分区

硬盘结构相关

阅读

Page 7: Linux 中的进程

7

3. 启动并使用机器① 加电开机② BIOS( ROM 中的 BIOS读 CMOS 中的参数,开

始硬件自检,找引导程序启动系统)③ 存在硬盘主引导扇区 MBR 里的引导程序被启动,

装载操作系统内核程序④ 内核程序启动

了解内核启动过程需看 linux源代码,不同的内核版本启动相关的文件不同,感兴趣的同学可阅读相关资料。

详细参阅本页备注 内核启动相关阅读

Page 8: Linux 中的进程

8

如何从系统进程过渡到用户使用

总之,从源码分析看,内核经历关键的一些 .s(汇编程序)和 .c程序启动后,最后会开始用户进程的祖先—— init。

init进程在 Linux操作系统中是一个具有特殊意义的进程,它是由内核启动并运行的第一个用户进程,因此它不是运行在内核态,而是运行在用户态。它的代码不是内核本身的一部分,而是存放在硬盘上可执行文件的映象中,和其他用户进程没有什么两样。

那么如何从内核过渡到 init进程?见如下示意图:

Page 9: Linux 中的进程

后面学习完 fork 等系统调用后再返回头看这里你会理解更多

0 号进程

1 号内核线程

调用 kernel_thread

调用 init ()

利用 execve ()从文件 /etc/inittab 中装入可执行程序 init

1 号用户进程 init

追根溯源:0 号进程——系统引导时自动形成的一个进程,也就是内核本身,是系

统中后来产生的所有进程的祖先。

所有进程的祖先

所有用户进程的祖先

0 号进程

1 号内核进程

Page 10: Linux 中的进程

10

当用户进程 init 开始运行,就开始扮演用户进程的祖先角色,永远不会被终止。所以:

计算机上的所有进程都是有上下亲属关系的,他们组成一个庞大的家族树。

观察 linux 下的进程间父子关系 :• pstree

– 以树状结构方式列出系统中正在运行的各进程间的父子关系。

• ps ax -o pid,ppid,command

Page 11: Linux 中的进程
Page 12: Linux 中的进程

12

二、 二、 linuxlinux 下的用户进程编程下的用户进程编程二、 二、 linuxlinux 下的用户进程编程下的用户进程编程

进程运行与内存密不可分,进程: pcb+ 代码段 + 数据段(数据 + 堆栈

)系统确信 init 进程总是存在的,用户

进程如果出现父进程结束而子进程没有终止的情况,那么这些子进程都会以 init 为父进程,而 init 进程会主动回收所有其收养的僵尸进程的内存。

Page 13: Linux 中的进程

Linux进程状态及转换

fork()

TASK_RUNNING就绪

TASK_INTERRUPTIBLE浅度睡眠

TASK_UNINTERRUPTIBLE深度睡眠

TASK_STOPPED暂停

TASK_ZOMBIE僵死

占有CPU执行

do_exit()schedule()

ptrace()

schedule() 当前进程时间片耗尽

等待资源到位sleep_on()schedule()

等待资源到位interruptible_sleep_on()

schedule()

资源到位wake_up_interruptible()

或收到信号wake_up()

资源到位wake_up()

收到信号SIGCONT

wake_up()

linux 进程状态

Page 14: Linux 中的进程

14

进程生命周期中的系统调用进程生命周期中的系统调用

Fork()-父亲克隆一个儿子。执行 fork()之后,兵分两路,两个进程并发执行。Exec()-新进程脱胎换骨,离家独立,开始了独立工作的职业生涯。Wait()-等待不仅仅是阻塞自己,还准备对僵死的子进程进行善后处理。Exit()-终止进程,把进程的状态置为“僵死”,并把其所有的子进程都托付给 init进程,最后调用schedule()函数,选择一个新的进程运行。

参考资料: Linux C编程一站式学习 .pdf

Page 15: Linux 中的进程

15

相关头文件• unistd.h

– 用于系统调用, Unix Standard的意思,里面定义的宏一类的东西都是为了 Unix标准服务的(一般来说包括了 POSIX的一些常量……)

• stdlib.h– 该文件包含了的 C 语言标准库函数的定义,定义了五种类型、

一些宏和通用工具函数。 类型例如size_t 、 wchar_t 、 div_t 、 ldiv_t 和 lldiv_t; 宏例如 EXIT_FAILURE 、 EXIT_SUCCESS 、 RAND_MAX 和MB_CUR_MAX等等; 常用的函数如 malloc() 、 calloc() 、realloc() 、 free() 、 system() 、 atoi() 、 atol() 、rand() 、 srand() 、 exit()等等。 具体的内容你自己可以打开编译器的 include目录里面的 stdlib.h头文件看看。

• linux常用 C 头文件列表见本页备注

Page 16: Linux 中的进程

16

1.fork1.fork ()()1.fork1.fork ()()

调用 fork 程序运行就发生分叉,变成两个控制流程,这也是“ fork” (分叉)名字的由来。

• 子进程克隆父进程父子进程内存空间代码相同,除非儿子用

exec 另启门户做其他工作。• 一次调用,两个返回值

fork 调用后,系统会在子进程中设置 fork 返回值是 0 ,而父进程内存空间中 fork 的返回值则是子进程的 pid 。

Page 17: Linux 中的进程

17

内存内核空间

PCB-father

用户空间

父进程pid_t = ***

PCB-child

子进程pid_t = 0

Page 18: Linux 中的进程

18

多次执行,测试结果并进行分析,体会进程并发

int main(void){ pid_t pid; char *message; int n; pid = fork(); if (pid < 0)

{ perror("fork failed"); exit(1); }

if (pid == 0) { message = "This is the child\n"; n = 6; }

else { message = "This is the parent\n"; n = 3; }

for(; n > 0; n--) { printf(message); sleep(1); } return 0;}

#include <sys/types.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>

Page 19: Linux 中的进程

19

区别 fork 和 vfork(选看 )• 空间的复制Fork :子进程拷贝父进程

的数据段Vfork:子进程 与父进程

共享数据段• 调度的顺序取决于调度算法。但vfork代码中会阻塞父进程先调度子进程。

#include <unistd.h>#include <stdio.h>Int main(void){ pid_t pid;int count=0;pid=vfork();count++;printf(“count=%d\n”,count);exit( 0 );return 0;}

Pid=fork();Count++;Printf(“count=%d\n”,count); 注意,使用 vfork ,若不用

exit ,进程无法退出。

Page 20: Linux 中的进程

20

• 关于并发顺序父子进程并发, linux优先调度执行子进程比较好。分析:如果先调父进程1.因为 fork将父进程资源设为只读,只要父进程进行修改,就要开始“写时复制”,把父进程要改的页面复制给子进程(写子空间)。

2.继续运行,一旦子进程被调度到,它往往要用 exec载入另一个可执行文件的内容到自己的空间(又写子空间),可见上步的写入就多余了。

所以, fork后优先调度子进程可从父进程克隆到子进程后,尽量减少没必要的复制。

Page 21: Linux 中的进程

21

* 关于 fork的 gdb调试跟踪 *• fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个 file结构体。

• 用 gdb调试多进程的程序会遇到困难, gdb只能跟踪一个进程(默认是跟踪父进程),而不能同时跟踪多个进程,但可以设置 gdb在 fork之后跟踪父进程还是子进程:– set follow-fork-mode child命令设置 gdb

在 fork之后跟踪子进程( set follow-fork-modeparent则是跟踪父进程),然后用 run命令,看到的现象是父进程一直在运行,在 (gdb)提示符下打印消息,而子进程被先前设的断点打断了。

Page 22: Linux 中的进程

22

思考题• 若一个程序中有这样的代码,则有几个进

程,父子关系如何?pid_t pid1,pid2;

pid1=fork();

pid2=fork();pid1>0 pid1=0

pid2>0 pid2=0

pid2=0 pid2>0

Page 23: Linux 中的进程

23

2.exec2.exec ()()2.exec2.exec ()()

• exec函数族包括若干函数:#include <unistd.h>int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ..., char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execve(const char *path, char *const argv[], char *const envp[]);

path 要执行的程序名(有或没有全路径) arg 被执行程序所需的命令参数,以 arg1, arg2, arg3…形式表示, NULL 为结束

argv 命令行参数以字符串数组 argv形式表示 envp 环境变量字符串

Page 24: Linux 中的进程

24

子进程用 exec 另做工作的举例

path arg2

arg1

Page 25: Linux 中的进程

25

实际上,只有 execve 是真正的系统调用,无论是哪个 exec函数,都是将要执行程序的路径、命令行参数、和环境变量 3 个参数传递给 execve ,最终由系统调用 execve完成工作。 p: 利用 PATH环境变量查找可执行的文件; l:希望接收以逗号分隔的形式传递参数列表,列表以 NULL

指针作为结束标志; v :希望以字符串数组指针( NULL 结尾)的形式传递命令行参数;

e:传递指定参数 envp ,允许改变子进程的环境,后缀没有 e 时使用当前的程序环境

Page 26: Linux 中的进程

26

• 注意点:– 子进程调用 exec使地址空间被填入可执行文

件的内容,子进程的 PID 不变,但进程功能开始有别于父进程。

– 注意 exec函数执行成功就会进入新进程执行不再返回。所以子进程代码中 exec 后的代码,只有 exec 调用失败返回 -1才有机会得到执行。

Page 27: Linux 中的进程

27

• execl举例#include <unistd.h>main(){execl (“/bin/ls” ,”ls”,”-al”,”/etc/passwd ”, NULL);}

• execlp举例#include <unistd.h>main(){execlp (“ls” ,”ls”,”-al”,”/etc/passwd ”,NULL);}

• execv举例#include <unistd.h>main(){char *argv[ ]={”ls”,”-l”,”/etc/passwd ”, (char *) 0};execv(“/bin/ls” ,argv);}

Page 28: Linux 中的进程

28

3.exit3.exit ()()3.exit3.exit ()()

void exit(int status);

• 程序执行结束或调用 exit 后,进程生命就要终结,但进程不是马上消失,而是变为僵死状态——放弃了几乎所有内存空间,不再被调度,但保留有 pcb 信息供 wait 收集,包括:正常结束还是被退出占用总系统 cpu 时间和总用户 cpu 时间缺页中断次数,收到信号数目等

• 利用参数 status传递进程结束时的状态

Page 29: Linux 中的进程

29

分析下面程序中的“僵尸”#include <sys/types.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>main(){

pid_t pid;pid=fork();if (pid<0) printf(“fork error!\n”);if (pid==0) /* 子进程 //sleep(10);

if (pid>0) { /* 父进程

sleep(20);}}

问:子进程一被调度到就结束成僵死态。谁来回收其 pcb ?

问:父进程被调度执行到最后,也会隐式结束成僵死态。谁来回收其 pcb ?

执行:gcc –o mywait mywait.c./mywait&ps -x (可看到状态为 Z 的僵尸进程)

问:若注释掉父进程的sleep语句,让子进程被调度后 sleep ,会是什么情况?给父子进程加上合适的输出观察。

printf(“child is %d,father is %d\n”,getpid(),getppid());

printf(“I’m father %d, my father is %d\n”,getpid(),getppid());

Page 30: Linux 中的进程

30

• 孤儿进程问题父进程在子进程前退出,必须给子

进程找个新父亲,否则子进程结束时会永远处于僵死状态,耗费内存。– 在当前进程 / 线程组内找个新父亲– 或者,让 init 做父亲

– 僵尸进程只能通过父进程 wait 回收它们,他们是不能用 kill 命令清除掉的,因为 kill 命令只是用来终止进程的,而僵尸进程已经终止了。

Page 31: Linux 中的进程

31

4.wait4.wait4.wait4.wait

pid_t wait(int *status)阻塞自己,等待第一个僵死子进程,进行下面

操作,否则一直阻塞下去。• 收集僵死子进程信息• 释放子进程 pcb ,返回

调用成功,返回被收集子进程的 PID;如果没有子进程,返回 -1 。

Page 32: Linux 中的进程

32

main(){

pid_t pc,pr;pc=fork();if (pc<0) printf(“fork error!\n”);if (pc==0){ /* 子进程 printf(“child process with pid of %d\n”,getpid());sleep(10);}if (pc>0){ /* 父进程pr=wait(NULL);printf(“catch a child process with pid of %d\n”,pr);}exit(0);

}

包含的头文件:#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdlib.h>

程序执行线路描述

问:父进程加或不加 wait 有什么区别?无论是否调用 wait ,如果在父亲离开时存在僵死子进程,父亲都会收集其 pcb 信息,并将其彻底销毁后返回。但加 wait还可起同步作用,保证子进程没结束前,父亲不会结束,注意这里只是一个儿子,若有两个儿子,情况又不同。

Page 33: Linux 中的进程

33

• 观察父亲对两个儿子的僵死处理对上面的代码做一些修改,如下main(){

pid_t p1,p2, pr;p1=fork();

p2=fork();if (p1==0){ /* 子进程printf(“NO.1 child process with pid of %d is going to sleep \n”,getpid());sleep(10);printf(“NO.1 child :my father is %d \n”,getppid()); }

if (p2==0){ /* 子进程 printf(“NO.2 child process with pid of %d is going to exit \n”,getpid());exit(0); } /* 父进程if (pc>0){ pr=wait(NULL);printf(“catch child process with pid of %d and I’m leaving!\n”,pr);}

} 问:父亲的 wait 是否等两个儿子都走了才走?会被先走的儿子触发,然后就离开,留下睡觉的儿子变成别人的儿子。

Page 34: Linux 中的进程

34

• wait起到了同步的作用,父进程只有当子进程结束后才能继续执行。

• 子进程退出时的状态会存入 wait 的整型参数status 中。由于相关信息在整数的不同二进制位上, wait 收集相关信息是利用定义的一套专门的宏。

Page 35: Linux 中的进程

多个子进程 分析试试看

pd>0 pd=0

pd1>0 pd1=0

pd1=0 pd1>0

等待收集 pd 子进程的死亡信息

等待收集 pd1 子进程的死亡信息利用 stat 分析 pd 子进程是正常结束还是异常死

亡利用 stat1 分析 pd1 子进程是正常结束还是异常死亡

Page 36: Linux 中的进程

36

运行测试:gcc –o mywait mywait.c

./mywait&& 符号让本程序后台执行,则当前 shell仍能响应命令程序后台执行中用“ kill -9 pid 号” 结束

子进程 ,试试看结果如何 . waitpid 参数 0换成 WNOHANG效果如何

* 代码中出现的 waitpid函数的具体使用自己查资料

Page 37: Linux 中的进程

37

进程的一生进程的一生随着一句 fork,一个新进程呱呱落地,但

这时它只是老进程的一个克隆。然后,随着 exec,新进程脱胎换骨,离家独立,开始了独立工作的职业生涯。

人有生老病死,进程也一样,它可以是自然死亡,即运行到 main函数的最后一个 "}",从容地离我们而去;也可以是中途退场,退场有 2 种方式,一种是调用 exit函数,一种是在 main函数内使用 return,无论哪一种方式,它都可以留下留言,放在返回值里保留下来;甚至它还可能被谋杀,被其它进程通过另外一些方式结束它的生命。

进程死掉以后,会留下一个空壳, wait站好最后一班岗,打扫战场,使其最终归于无形。这就是进程完整的一生。

Page 38: Linux 中的进程

38

( 1 )写一个包含两次 fork的程序,通过代码给出合适的可以观察到父子 PID及父子关系的输出。

( 2 )观察父 exit子 sleep和父 sleep子 exit的进程运行效果,并说明每个进程什么时候是僵死态,如何利用 ps观测到僵死态的进程。

• 要求: 1 )写出代码,利用 sleep、 printf等让进程给出合适的输出提示。

2 )给出你的运行测试步骤。 3 )运行结果是什么,你分析程序是怎么执行的,给出说明。

实验名称:进程操作的实验名称:进程操作的 44 个系统调用个系统调用实验名称:进程操作的实验名称:进程操作的 44 个系统调用个系统调用

Page 39: Linux 中的进程

39

三、三、 linuxlinux 信号量操作信号量操作三、三、 linuxlinux 信号量操作信号量操作

操作系统需要解决进程之间资源合理分配的问题, Linux采用信号量( Semaphore )来解决这一问题,一个信号量表示可用资源的数量 。

信号量操作函数定义的头文件:#include <sys/sem.h>

Page 40: Linux 中的进程

40

温故知新• 信号量

– 整型、记录型、信号量集• 对信号量有两种操作

– wait(S) :信号量的值 S=S-1 ,如果 S0 ,则正常运行,如果 S<0 ,则进程暂停运行进入等待队列。

– signal(S) :信号量的值 S=S+1 ,如果 S>0 ,则正常运行,如果 S0 ,则从等待队列中选择一个进程使其继续运行,进程 V 操作的进程仍继续运行。

Page 41: Linux 中的进程

41

• 信号量实现互斥Semaphore s=1;wait(s);

使用打印机及;signal(s);

• 信号量集 一个信号量集里包含对若干个信号量的处理

– sswait( s,1,1;d,1,0 )表示要申请两个信号量 s、 d 。两类资源允许申请的资源下限都是 1, s 要求申请 1 个, d 要求申请 0 个。

– 信号量集 sswait(x,1,1) 等价于信号量操作。

Page 42: Linux 中的进程

42

linuxlinux 信号量集操作函数信号量集操作函数linuxlinux 信号量集操作函数信号量集操作函数

1. semgetint semget(key_t key, int nsems, int semflg); 创建、打开一个已创建的信号量集。

2. semopint semop(int semid, struct sembuf *sops,

unsign ednsops); 对信号量集中指定的信号量进行指定的操作。

3. semctlint semctl(int semid, int semnum, int

cmd, ...); 对信号量集中指定的信号量进行控制操作。

Page 43: Linux 中的进程

43

1. semget创建或打开一个已创建的信号量集,执行成功会返回信号量的 ID, 否则返回 -1 ;int semget(key_t key, int nsems, int semflg); m=semget(IPC_PRIVATE,1,0666|IPC_CREAT);

-----------------------------------------– key 创建或打开的信号量集的键值,常用 IPC_PRIVATE,

由系统分配。– nsems 新建信号量集中的信号量个数,通常为 1 ;– semflg 对信号量集合的打开或存取操作依赖于

semflg参数的取值:IPC_CREAT :如果内核中没有新创建的信号量集合,则创建它。 IPC_EXCL : IPC_EXCL单独是没有用的,要与IPC_CREAT结合使用,要么创建一个新的集合,要么对已存在的集合返回 -1。可以保证新创建集合的打开和存取。作为 System V IPC的其它形式,一种可选项是把一个八进制与掩码或,形成信号量集合的存取权限。

Page 44: Linux 中的进程

44

2. semop借助 sembuf 结构体对指定的信号量进行指定的操作 ,增加或减少信号量值,对应于共享资源的释放和占有。执行成功返回 0 ,否则返回 -1 。int semop(int semid, struct sembuf *sops, unsigned nsops); struct sembuf sem_b;

sem_b.sem_num = 0;sem_b.sem_op= -1;sem_b.sem_flg=SEM_UNDO;

semop(m,&sem_b,1);

-------------------------------------– semid 信号量集的 id– sops 指向对信号量集中的信号进行操作的数组,数组类型为 sembuf。– nsops 指示 sops数组的大小– 关于 struct sembuf {

ushort sem_num;//要操作的信号量在信号量集的索引值 short sem_op; //负数表示 P 操作,正数表示 V 操作 short sem_flg; //操作标志, SEM_UNDO,进程意外结束时,恢复信号量操作。

}; 示例代码可解释为:利用 sem_b结构对 m 信号量集做操作, sem_b只有 1 个长度,所以意味着就做1 个操作, sem_b中定义的操作是对信号量集m 的第 1个信号做P 操作,如果程序意外退出,为防止信号量没释放造成的死锁,会将已做的 P 操作 UNDO。

思考: semop( m,&sem_b,2) ,sem_b.sem_num=1什么意思?

Page 45: Linux 中的进程

45

3.semctl对信号量属性进行操作 ( 比如信号量的赋初值),调用成功返回返回结果与 cmd 相关,调用失败返回 -1int semctl(int semid, int semnum, int cmd, union

semun arg); semctl(m,0,SETVAL,1);

-------------------------------------------– semid 信号量集的标识号– semnum 要操作的信号量集中信号量的索引值,对于集合上的第一个信号量,该值为 0 。

– cmd 表示要执行的命令,这些命令及解释见下页表

– arg 与 cmd搭配使用,类型为 semun– 关于 union semun ( include/linux/sem.h中定义) {

int val; //只有在 cmd=SETVAL时才有用

struct semid_ds *buf;//IPC_STAT IPC_SET的缓冲

ushort *array; // GETALL & SETALL 使用的数组

…}

* 示例代码直接利用常数 1 给信号量设置了值。从 cmd参数结合内核代码可以看到 semun还能用于消息队列通信等操作。

Page 46: Linux 中的进程

46

• semctl中 cmd 参数的命令及解释

Page 47: Linux 中的进程

47

互斥的例子int room = 0;char ch;int main(){ pid_t pid; pid_t pids[2]; int i=0; int j=0;

room=semget(IPC_PRIVATE,1,0666|IPC_CREAT);semctl(room,0,SETVAL,1);

for (i=0;i<2;i++) { pid=fork(); if (pid==0){

while(1){…} } else{ pids[i]=pid;} }

do{ printf(“press q to exit\n"); ch=getchar(); if (ch == 'q') for (i=0;i<2;i++)

kill(pids[i],SIGTERM); }while(ch != 'q');}

while(1){printf("%d want to enter room--P\n",i);

struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op= -1;

sem_b.sem_flg=SEM_UNDO;

semop(room,&sem_b,1);printf("%d is in room\n",i);sleep(6);printf("%d is want to leave room--V\n",i); sem_b.sem_op=1;

semop(room,&sem_b,1);printf("%d is out of room\n",i);}//while

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <errno.h>#include <fcntl.h>#include <signal.h>

Page 48: Linux 中的进程

48

实例训练实例训练————哲学家就餐哲学家就餐实例训练实例训练————哲学家就餐哲学家就餐

五位哲学家围坐在一张圆形桌子上,桌子上有一盘饺子。每一位哲学家要么思考,要么等待,要么吃饺子。为了吃饺子,哲学家必须拿起两只筷子,但是每个哲学家旁边只有一只筷子,也就是筷子数量和哲学家数量相等,所以每只筷子必须由两个哲学家共享。设计一个算法以允许哲学家吃饭。算法必须保证互斥(没有两位哲学家同时使用同一只筷子)

同时还要避免死锁(每人拿着一只筷子不放,导致谁也吃不了)

Page 49: Linux 中的进程

49

避免死锁的方法• 限制同时吃饭的哲学家数,下面例子中同

时只允许 4 个哲学家同时吃饭;• 或者通过给所有哲学家编号,奇数号的哲学家必须首先拿左边的筷子,偶数号的哲学家则首先拿右边的筷子来避免死锁。

Page 50: Linux 中的进程
Page 51: Linux 中的进程

1) 先实现 P、 V 原语2) 再编写主程序代码,通过 fork 创建哲学家进程

/* P原语 */int Psem(int sem_id){

/* 设计操作结构体 */struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = -1;sem_b.sem_flg = SEM_UNDO;

 /* 调用库函数,进行 P操作 */if(semop(sem_id,&sem_b,1) == -1){

fprintf(stderr, "P failed\n");return 0;

}return 1;

}

/* V 原语 */int Vsem(int sem_id){

/* 设计操作结构体 */struct sembuf sem_b;sem_b.sem_num = 0;sem_b.sem_op = 1;sem_b.sem_flg = SEM_UNDO;

 /* 调用库函数,进行 V 操作 */if(semop(sem_id,&sem_b,1) == -1){

fprintf(stderr, "V failed\n");return 0;

}return 1;

}

Page 52: Linux 中的进程

关键代码分析 Psem、 Vsem 房间只能进 4 个人,防止死锁int room_sem_id = CreateSem(4); 每个筷子一个信号量

for (i = 0; i < 5; i++) {chopsticks_sem_id[i] = CreateSem(1); }

5 个进程,每个代表一个哲学家for (i = 0; i < 5; i++) {

pid = fork();……Psem(room_sem_id);Psem(chopsticks_sem_id[i] );……Vsem(chopsticks_sem_id[i] );Vsem(room_sem_id);

Page 53: Linux 中的进程

53

• 编译与运行 gcc -o philosopher main.c mysemop.c

Page 54: Linux 中的进程

54

同步的例子• 一个盘子放 1 个水果,父亲放,儿子吃。

semaphore e=1, f=0;父亲 儿子p( e ) p( f )

放水果 取水果v( f ) v( e )

Page 55: Linux 中的进程

55

实验名称:实验名称: linuxlinux 信号量集实现吃水果问信号量集实现吃水果问题题实验名称:实验名称: linuxlinux 信号量集实现吃水果问信号量集实现吃水果问题题

• 一个盘子可放 1 个水果,父亲放梨,儿子吃梨;母亲放桔,女儿吃桔。要求 1)写出代码,注意给出合适的输出提示。 2)运行结果是什么,你分析程序是怎么执行的,给出说明。

Page 56: Linux 中的进程

56

思考与练习 思考与练习 思考与练习 思考与练习 1 .分析进程和线程的关系,查阅资料,浅析Windows

进程管理和 Linux 进程管理的异同之处。2.分析进程的各个状态。准备运行和阻塞都是进程在运行过程中没有得到 CPU的状态,它们有什么异同之处。

3 .根据下面的故事,编写演示程序:• 一盘子能装 3 个水果,父亲随机放水果(挑选橘子或苹

果,剥皮后放入盘子中,剥橘子皮的速度较快,而削苹果皮的速度较慢。)女儿不断吃橘子,儿子不断吃苹果。儿子吃的较快,女儿吃的较慢。

Page 57: Linux 中的进程

57

以下可做扩展阅读

Page 58: Linux 中的进程

58

硬盘结构(兴趣阅读)例: 2个主分区(其中第 1个是引导分区), 1个扩展分区(分成 2个逻辑分区)。

系统安装过程会将系统启动程序写入 MBR,操作系统程序写入硬盘。

MBR

DPT

DBR

FAT

目录

OS数据 D

BR

FAT

目录

EBR

DBR

FAT

目录

DBR

FAT

目录

主引导扇区

主引导分区

Page 59: Linux 中的进程

59

内核启动相关的文件linux2.4内核

./arch/x86/boot/bootSect.S ./arch/x86/boot/setup.S ./arch/x86/kernel/head.S ./init/main.c

BIOS找到启动设备第 1 扇区中的引导程序,再引导内核– linux2.4: /usr/src/linux-2.4.2/arch/i386/boot/bootsect.S

的汇编语言文件– Intel系统里,用得最多的自举程序就是 LILO、 GRUB等。对于其它的体系

结构,还存在着别的自举程序。一般 MBR空间有限,只存放 LILO的一部分a ),剩余部分由 a )自己继续装入内存。

linux2.6内核1. CPU加电后,自动从 0xFFFF0处开始执行代码,这个地址位于 BIOS中。2. BIOS开始一系列系统检测,并在内存物理地址 0 处初始化中断向量,供

Linux内核启动过程中进行调用。3. 将启动设备的第一个扇区(磁盘引导扇区, 512Bytes)读入内存绝对地址

0x7C00处,并跳到这个地方开始执行( arch/i386/boot/header.s)。 • 程序现在运行在 16位实模式下,使用 64k的段。 segment(段地址 )

: offset(偏移 ) 构成了逻辑地址,段地址乘以 16 再加上 偏移 ,就得到 linearaddress(线性地址 )

4. 初始化 Disk Controller(磁盘控制器 ) ,是通过 int 0x13进行的。然后设置寄存器,初始化数据段,接着 call main跳转到: arch/i386/boot/main.c的 main()函数开始执行。

Page 60: Linux 中的进程

60

启动时内存中的变化

•初始化时 setup需要绝对地址 0开始处 1k的中断向量表用放 BIOS相关中断参数,所以 system一开始不直接加载到 0 。而是后面由 setup移动。•system移动到位,相应的保护模式各种信息也准备好了, head调用main开始执行。

Page 61: Linux 中的进程

61

内核源代码的获取 •内核获取途径

– Linux内核官方网站 http://www.kernel.org

–国内镜像•发行版已带内核源码的,一般安装在 /usr/src/linux目录下

•安装内核源码– GNU zip( gzip)格式内核解压命令– $tar xvzf linux-x.y.z.tar.gz– bzip2格式解压命令– $tar xvjf linux-x.y.z.tar.bz2

–解压后内核源码位于 linux-x.y.z目录下

Page 62: Linux 中的进程

62

内核源码目录结构• /usr/src/linux

– arch : 核心源代码所支持的硬件体系结构相关的核心代码。如对于 X86平台就是 x86。

– include : 核心的大多数 include文件。另外对于每种支持的体系结构分别有一个子目录。

– init : 核心启动代码。 – mm : 内存管理代码。与具体硬件体系结构相关的内存管理代码位于 arch/*/mm目录下,如对应于 X86的就是 arch/x86/mm/fault.c 。

– drivers : 设备驱动都位于此目录中。它又进一步划分成几类设备驱动,每一种也有对应的子目录,如声卡的驱动对应于 drivers/sound。

– ipc : 核心的进程间通讯代码。

Page 63: Linux 中的进程

63

– modules : 包含已建好可动态加载的模块。

– fs Linux: 支持的文件系统代码。不同的文件系统有不同的子目录对应,如 ext2文件系统对应的就是 ext2子目录。

– kernel : 主要核心代码。同时与处理器结构相关代码都放在 arch/*/kernel目录下。

– net : 核心的网络部分代码。里面的每个子目录对应于网络的一个方面。

– lib : 核心的库代码。与处理器结构相关库代码被放在 arch/*/lib/目录下。

– scripts: 用于配置核心的脚本文件。 – Documentation :一些文档,起参考作用。

Page 64: Linux 中的进程

64

Page 65: Linux 中的进程

65

内核中使用的 C 语言 (GNU C)

• 内核的主体是以 GNU的 C 语言编写的, GNU对 C 语言本身在 ANSI C的基础上作了不少扩充

• 内核开发者使用的 C 语言涵盖了 ISO C99标准和 GNU C扩展特性

• 参考网站–http://www.faqs.org/docs/learnc/

–http://gcc.gnu.org/onlinedocs/gcc-4.0.0/gcc/C-Extensions.html#C-Extensions

Page 66: Linux 中的进程

66

Linux 内核中的汇编语言代码 • 为什么使用汇编语言?

– C 语言没有对应的硬件操作语言,如 inb, outb等– C 语言没有对应的 CPU特殊指令,如开关中断、寄存器操作等

– 提高时间效率,如系统调用的陷入或返回– 提高空间效率,如系统第一扇区的引导程序

• Linux源代码中汇编语言的使用形式– 完全汇编代码– 嵌在 C 语言程序的汇编片断– 几个用于引导的 Intel格式的汇编语言程序

• Linux使用的编译器是 GNU的 gcc,所以源代码的汇编大多是 AT&T格式的 GNU汇编语言,与 Intel格式有所的区别。

Page 67: Linux 中的进程

67

• Linux 开发模式– LINUX

• POSIX、 GNU、 GPL• 开发模式:遵循 GPL ,开放源代码、利用

internet ,通过 BBS 、新闻组及电子邮件,集体协作地开发。

• 自由程序员提交代码,只有领导者才有权限决定把哪些代码合并到正式核心版本中。

• 大量用户测试、高经验程序员裁决, linux 的正式发布代码质量很高。

Page 68: Linux 中的进程

68

• 单内核–整个系统是一个大模块,模块间交互时直接

调用其他模块的函数。注重执行效率。• 微内核

–服务尽可能的分离出内核,变成相对独立的模块,模块、微内核通过通信机制交互,相对效率较低些,但方便修改维护。

–尚处于发展阶段。

Page 69: Linux 中的进程

69

• 传统 Unix 内核都是单内核结构,几乎都要硬件提供页机制以管理内存

• Linux即不是层次结构,也不是微内核结构,是单内核结构,但也有类似微内核的优点–模块化设计–抢占式内核–支持内核线程– 动态装载内核模块

Page 70: Linux 中的进程

70

Linux 源代码的阅读方法• 阅读顺序

–纵向:顺着程序的执行顺序逐步进行。–横向:分模块进行

• 划分不是绝对的,而是经常结合在一起进行– Linux 的启动:顺着 linux 的启动顺序读,大致流程

如下(以 X86平台为例):./arch/x86/boot/bootSect.S./arch/x86/boot/setup.S./arch/x86/kernel/head.S./init/main.c 中的 start_kernel()

– 对于内存管理等部分,可以单独拿出来按模块进行阅读分析。

Page 71: Linux 中的进程

71

• 阅读工具帮助追踪复制、调用、数据结构定义等– windows环境下利用 Source Insight– linux环境下利用 lxr(linux cross

reference)或 glimpse 等• 开始

– 一般按顺序阅读启动代码;–然后进行专题阅读,如进程部分,内存管理部分等。在每个功能函数内部应该一步步来

–想要了解操作系统内部奥秘,反复读吧。