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

Linux 中的进程

Embed Size (px)

DESCRIPTION

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

Citation preview

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 等• 开始

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

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