Linux 多进程编程:
1. Linux下进程的结构:
Linux下一个进程在内存里有三部分的数据:数据段,堆栈段,代码段.
代码段存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们可以使用同一个代码段.
堆栈段存放子程序(注意是子程序)的返回地址,子程序的参数以及程序的局部变量.
数据段存放全局变量,常熟以及动态数据分配的数据空间(如用malloc之类的函数取得的空间.)
上面说了,数个进程运行相同的一个程序他们可以使用同一个代码段,但是不能使用同一个堆栈段和数据段.
2.系统调用产生新进程-fork()
在Linux下产生新的进程的系统调用就是fork函数,这个函数名是英文中“分叉”的意思。为什么取这个名字
呢?因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就“分叉”了,所以这个名字取得很形象。这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样.就象符进程克隆 (clone)自己一样.当然创建两个一模一样的进程是没有意义的.为了区分父进程和子进程,我们必须跟踪fork的返回值. 当fork掉用失败的时候 (内存不足或者是用户的最大进程数已到)fork返回-1,否则fork的返回值有重要的作用.对于父进程fork返回子进程的ID,而对于fork子进程返回0.我们就是根据这个返回值来区分父子进程的.。
注意:fork()调用时是子进程先返回还是父进程先返回不确定,有操作系统控制.
深入分析:
一个程序一调用fork函数,系统就为一个新的进程准备了上面说的三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了.
疑问:
如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?
回答:
其实UNIX自有其解决的办法,大家知道,一般CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在
通常情况下是4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在fork之后经常跟真exec,作为替代使用了写时复制的技术(copy-on-write,COW),这些区域由父子进程共享,而且内核将它们的存取许可权改变为只读的,如果有进程试图修改这些区域,则内核为有关部分,典型的是虚拟存储系统中的“页”做一个拷贝。
一般在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核使用的调度算法,如果要求父子进程之间相互同步,则要求某种形式的进程间通讯。
Linux的系统调用函数write是不带缓存的,而标准I/O库是带缓存的。如果标准库输出到终端设备,那么它是行缓存的,否则是全缓存的。标准输出缓存由新行符刷新
文件共享:
父子进程共享一个文件表项---即父子进程对同一个文件使用了一个文件位移量
fork()的例子:
- #include
- #include /*fork()函数所在的头文件*/
- int main()
- {
- printf("before fork()!\n");
- pid_t pid = fork();
- printf("after fork()!\n");
- if(pid < 0)
- {
- printf("fork() error!");
-
- }
- else if(pid ==0)
- {
- printf("Child process is printing!\n");
- /*打印子进程的id*/
- printf("This is child process,it's PID : %d\n", getpid());
-
- printf("This is child process,it's parent's PID : %d\n", getppid());
- }
- else
- {
- printf("Parent process is printing!\n");
- printf("This is parent process,it's PID : %d\n", getpid());
- printf("This is parent process,it's parent's PID : %d\n", getppid());
- }
- }
----------------------------
执行结果可能是:(父进程先返回)
[zzz@localhost process]$ ./a.out
before fork()!
after fork()!
Parent process is printing!
This is parent process,it's PID : 3106
This is parent process,it's parent's PID : 2604
after fork()!
Child process is printing!
This is child process,it's PID : 3107
This is child process,it's parent's PID : 1
还可能是:(子进程先返回)
before fork()!
after fork()!
Child process is printing!
This is child process,it's PID : 6979
This is child process,it's parent's PID : 6978
after fork()!
Parent process is printing!
This is parent process,it's PID : 6978
This is parent process,it's parent's PID : 2245
-------------------------------------------------------
注意上面的结果:第一种结果子进程的父进程id打印的是1,而不是3106,为什么?
因为父进程比子进程提前终止了!!那么子进程就变成孤儿进程了,那么它就由INIT进程收养!!所以父进程是1,init进程是系统初始化的进程.
第二种结果:子进程先返回,父进程还没终止,所以打印出了父进程的id是6978,和父进程自己打印的id一样.
3.vfork()函数
vfork函数的调用序列和返回值与fork相同,但语义不同。vfork用于创建一个新进程,该进程的目的是exec一个新进程。vfork和fork一样都创建一个子进程,
但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在
父进程的空间中运行。这种工作方式在某些UNIX的页式虚存中提高了效率
vfork和fork之间的另一区别是,vfork保证子进程先运行,它在调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖父进程的
进一步动作,则会导致死锁。
4.exit函数
进程有三种正常终止法和两种异常终止法
1>正常终止
a.在main函数中执行return语句,等效于调用exit
b.调用exit函数,该函数由ANSI C定义,其操作包括调用各终止处理程序,然后关闭所有标准I/O流,因为ANSI C不处理文件描述符,多进程以及作业控制
所以这一定义对UNIX是不完整的
c.调用_exit系统调用函数,它并不执行标准I/O缓存的刷新操作,它处理UNIX特定的细节,_exit是由POSIX说明的
2>异常终止
a.调用abort,它产生SIGABRY信号
b.当进程接收到某个信号时。
不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为进程关闭所有打开的文件描述符,释放它所使用的存储器等。
对于上面任何一种终止情况,我们都希望终止进程能够通知父进程它是如何终止的,对exit和_exit,是依靠传递给他们的退出状态实现的;在异常终止情况下,
内核(非进程本身)会产生一个指示其异常终止原因的终止状态,在任意一种情况下,该终止进程的父进程都能使用wait或waitpid函数获取其终止状态。
5.wait和waitpid函数
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号,因为子进程终止是一个异步事件,所以这种信号也是内核向父进程发出的异步通知。
调用wait和waipit可能会导致:
1>阻塞(当父进程的所有子进程都在运行时)
2>一个子进程已经终止,正等待父进程存取其终止状态
3>出错并立即返回(该进程没有任何子进程)
如果进程由于接收到SIGCHLD信号而调用wait则可期望wait立即返回,但是如果在一个任意时刻调用wait,可能会阻塞
#include
#include
pid_t wait(int &status);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
两个函数的区别是:
在一个子进程终止前,调用wait会使调用者阻塞,而waitpid有一选择项,可以使waitpid不阻塞。
waitpid并不等待第一个终止的子进程,可以控制它所等待的进程。
如果是一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait一直阻塞直到一个进程结束。
阅读(1150) | 评论(0) | 转发(0) |