Chinaunix首页 | 论坛 | 博客
  • 博客访问: 572746
  • 博文数量: 114
  • 博客积分: 1620
  • 博客等级: 上尉
  • 技术积分: 1104
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-30 09:16
文章分类

全部博文(114)

文章存档

2016年(1)

2015年(2)

2014年(4)

2013年(9)

2012年(20)

2011年(78)

分类: LINUX

2011-01-10 10:03:32

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()的例子:
  1. #include 
  2. #include /*fork()函数所在的头文件*/
  3. int main()
  4. {    
  5.     printf("before fork()!\n");/*这一句在fork()调用之前将只打印一次*/
  6.         pid_t pid = fork();
  7.     printf("after fork()!\n");/*这一句在fork()调用之后将打印两次*/
  8.         if(pid < 0)
  9.         {
  10.                 printf("fork() error!");
  11.                 //exit(1);
  12.         }
  13.         else if(pid ==0)
  14.         {
  15.                 printf("Child process is printing!\n");
  16.                 /*打印子进程的id*/
  17.                 printf("This is child process,it's PID : %d\n", getpid()); 
  18.                 /*打印(子进程的)父进程的id*/
  19.                 printf("This is child process,it's parent's PID : %d\n", getppid()); 
  20.         }
  21.         else
  22.         {
  23.                 printf("Parent process is printing!\n");
  24.                 printf("This is parent process,it's PID : %d\n", getpid());/*父进程id*/
  25.                 /*父进程的父进程的id*/       
  26.                 printf("This is parent process,it's parent's PID : %d\n", getppid());
  27.         }
  28. }
----------------------------
执行结果可能是:(父进程先返回)
[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) |
给主人留下些什么吧!~~