[关闭]
@shaobaobaoer 2018-09-23T06:06:30.000000Z 字数 6762 阅读 1142

Linux 进程管理及进程通信

Linux


一. 实验目的

利用Linux提供的系统调用设计程序,加深对进程概念的理解。
体会系统进程调度的方法和效果。
了解进程之间的通信方式以及各种通信方式的使用。

二. 实验准备

复习操作系统课程中有关进程、进程控制的概念以及进程通信等内容(包括软中断
通信、管道、消息队列、共享内存通信及信号量概念)。

三. 基础知识

进程

http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

线程

http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

信号量

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

消息队列

消息队列与命名管道类似,但少了打开和关闭管道方面的复杂性。使用消息队列并未解决我们在使用命名管道时遇到的一些问题,如管道满时的阻塞问题。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。

管道

管道通信方式的中间介质是文件,通常称这种文件为管道文件。两个进程利用管道文件进行通信时,一个进程为写进程,另一个进程为读进程。写进程通过写端(发送端)往管道文件中写入信息;读进程通过读端(接收端)从管道文件中读取信息。两个进程协调不断地进行写、读,便会构成双方通过管道传递信息的流水线。

管道分为匿名管道和命名管道。

(1)匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。

(2)命名管道:可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。

管道 VS 消息队列

优点:
      A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。
      B. 我们可以用一些方法来提前查看紧急消息。

缺点:
      A. 与管道一样,每个数据块有一个最大长度的限制。
      B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。

Linux系统中有两个宏定义:
 MSGMAX, 以字节为单位,定义了一条消息的最大长度。
 MSGMNB, 以字节为单位,定义了一个队列的最大长度。

四. 实验内容

LINUX 的进程管理与通信(控制部分)(一)

fock 的意思是复制进程,就是把当前的程序再加载一次,加载后,所有的状态和当前进程是一样的(包括变量)。fock后,新进程的入口就在fock的下一条语句。返回:子进程中为0,父进程中为子进程 ID,出错为-1

1. 创建子进程

  1. root@kali:~/thread_process# ./output/exam1.o
  2. This is Parent Process!
  3. This is Child Process!
  4. Child Complete!

思考:子进程是如何产生的? 又是如何结束的?子进程被创建后它的运行环境是怎样建立的?

执行到fork()函数,分出一个子进程,完全复制父进程的所有内容,并获取一个 pid 。随后,由于父进程优先于子创建,所以会先输出 parent process 。之后,子进程会继续往下执行,打印出 child 。wait 函数是在 子进程结束后执行的。当子进程结束的时候。打印出 child complete

2. 循环创建子进程-多分支

  1. root@kali:~/thread_process# ./output/exam2.o
  2. My pid is 6783 my father's pid is 5093
  3. 0 pid = 6784 ppid = 6783
  4. 1 pid = 6785 ppid = 6784
  5. 2 pid = 6786 ppid = 6785
  6. 6785 : The child 2 is finished.
  7. 6784 : The child 1 is finished.
  8. 2 pid = 6787 ppid = 6784
  9. 6784 : The child 2 is finished.
  10. 6783 : The child 0 is finished.
  11. 1 pid = 6788 ppid = 6783
  12. 2 pid = 6789 ppid = 6788
  13. 6788 : The child 2 is finished.
  14. 6783 : The child 1 is finished.
  15. 2 pid = 6790 ppid = 6783
  16. 6783 : The child 2 is finished.

进程家族树 PS 6787 的位置是在 i=2的地方

TIM截图20180923101609.png-163kB

一共产生了7对 8个进程,这样很没意义

2. 循环创建子进程-单分支

  1. root@kali:~/thread_process# ./output/exam2-2.o
  2. pid = 7052 ppid = 7051
  3. I am parent ; my pid = 7051 and my child is 7054
  4. pid = 7053 ppid = 7051
  5. pid = 7054 ppid = 7051

这样的话,相当于不允许子进程创建孙进程。 fork()如果发现其返回值是0 【也就是子进程返回的值】或者 -1 直接结束掉循环。

3. fork 与 exec 函数的运用

execlp函数的最后一个参数一定是(char *) NULL
execlp第一个参数是文件名,从第二个参数开始就是argv[0],argv1...
argv[0]就是程序自己的名字,然后开始是程序的参数。返回值,成功为 NULL 失败为 (int) -1
比如 execlp("ls", "ls","-l", "/home/cmstapp/",NULL);

  1. root@kali:~/thread_process# ./output/exam3.o
  2. Parent process is wating for child process
  3. shaobaobaoer Niu Bi !
  4. child process 1 finish with status 0
  5. root
  6. child process 2 finish with status 0
  7. uid=0(root) gid=0(root) 组=0(root)
  8. child process 3 finishi with status 0
  9. All child process finish
  10. parent process finish

思考:子进程运行其它程序后,进程运行环境怎样变化的?反复运行此程序看会有什么情况?

有时候指令会上浮?
问问老师看

LINUX 的进程管理与通信(同步部分)(二)

1. 观察父、子进程对变量处理的影响

  1. root@kali:~/thread_process# ./output/exam4.o
  2. before fork.
  3. As u can see child can't change the vari and globa
  4. pid = 8106 vari == 5 globa = 4
  5. Pid return 0
  6. Child 8107 changed the vari and globa.vari= 4 globa = 5

思考:子进程被创建后,对父进程的运行环境有影响吗?

由于fork()函数是把当前的程序再加载一次,加载后,所有的状态和当前进程是一样的(包括变量)。也就是说,fork子进程归子进程的,父进程归父进程的。两者无关系

2.管道通信

  1. root@kali:~/thread_process# ./output/exam5-1.o
  2. Send data to client, hello!
  3. Receive data from server, hello!

这里的close 和 open 其实就是lock

思考:锁能够不用么?
当然不可以,管道只能打开一端。如果不加锁的话,对于我上述这个代码,是没有问题的。但是如果存在着多个读写进程的话,会变得混乱。

3. 处理孤儿进程

  1. root@kali:~/thread_process# ./output/exam6-orphan.o
  2. I am father process.
  3. I am the child process.
  4. pid: 8743 ppid:8742
  5. I will sleep five seconds.
  6. father process is exited.
  7. root@kali:~/thread_process# pid: 8743 ppid:1622 # <--- 孤儿进程。
  8. child process is exited.

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

思考:对此作何感想,解释一下你的实现方法。

如果让我们自己去写的话,我们可以让该程序中的父进程变为子进程,子进程变为孙进程。然后父进程循环wait,来接收 ’孤儿‘ 进程。

4. 消息队列之间的通信

要求:客户将自己的进程标识(pid)通过消息机制发送给服务者进程。服务者进程收到消息后,将自己的进程号和父进程号发送给客户,然后返回。客户收到后显示服务者的pid 和ppid,结束。

一些基本函数

  1. root@kali:~/thread_process# ./output/exam7-server.o &
  2. [1] 9108
  3. root@kali:~/thread_process# ./output/exam7-client.o
  4. Enter some text: shaobao
  5. You wrote: shaobao
  6. Enter some text: hi
  7. You wrote: hi
  8. Enter some text: #end#
  9. You wrote: #end#
  10. [1]+ 已完成 ./output/exam7-server.o

LINUX 的进程管理与通信(同步与通信(软中断部分))(三)

signal 函数
void (* signal( int signo, void (*func)(int) ) )(int);

函数名 :signal
函数参数 :int signo, void (func)(int)
返回值类型:void (
)(int);

1. 软中断信号实验

  1. /* 父进程向子进程发送18 号软中断信号后等待。子进程收到信号,执行指定的程序,再将父进程唤醒。*/
  2. root@kali:~/thread_process# ./output/exam8.o
  3. I am Process 9830 ; it is signal 18 processing function.
  4. Parent:signal 18 has been sent to child 9831 ,returned 0.
  5. After wait 9831 ,Parent 9830 : finished.

1-plus. 理解信号机制

关键代码

  1. main{
  2. signal(SIGUSR1, sig_usr)
  3. signal(SIGUSR2, sig_usr)
  4. }
  5. static void sig_usr(int signo) /* argument is signal number */
  6. {
  7. if(signo == SIGUSR1)
  8. printf("received SIGUSR1\n");
  9. else if (signo == SIGUSR2)
  10. printf("received SIGUSR2\n");
  11. }
  1. root@kali:~/thread_process# ./output/exam8-1.o &
  2. [3] 10048
  3. root@kali:~/thread_process# kill -SIGUSR1 10048
  4. received SIGUSR1
  5. root@kali:~/thread_process# kill -SIGUSR2 10048
  6. received SIGUSR2
  7. root@kali:~/thread_process# kill 10048
  8. root@kali:~/thread_process# ls
  9. ...
  10. [3]- 已终止 ./output/exam8-1.o

用kill命令将信号传送给它。在UNIX中,杀死(kill)这个术语是不恰当的。kill命令和kill函数只是将一个信号送给一个进程或进程组。信号是否终止进程则取决于信号的类型,以及进程是否安排了捕捉该信号。当没有参数的时候才是默认杀死该进程。

此时,我们传递 -SIGUSR1 时候,会触发 signal(SIGUSR1, sig_usr) 函数,从而输出内容

2. 信号量机制解决生产者消费者问题

关键代码

  1. void provide_data(void) {
  2. int i;
  3. for (i=0; i< MAXSIZE; i++) {
  4. stack[i] = i;
  5. sem_post(&sem); //为信号量加1
  6. }
  7. }
  8. void handle_data(void) {
  9. int i;
  10. while((i = size++) < MAXSIZE) {
  11. sem_wait(&sem); //为信号量减1
  12. printf("乘法: %d X %d = %d\n", stack[i], stack[i], stack[i]*stack[i]);
  13. sleep(1);
  14. }
  15. }
  16. root@kali:~/thread_process# ./output/thread2_signal.o
  17. 乘法: 0 X 0 = 0
  18. 乘法: 1 X 1 = 1
  19. 乘法: 2 X 2 = 4
  20. 乘法: 3 X 3 = 9

五. 实验代码

http://shaobaobaoer.cn/cdn/thread&process.zip

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注