现在的位置: 首页 > 综合 > 正文

Linux操作系统分析(2) 进程的创建与可执行程序的加载

2013年03月29日 ⁄ 综合 ⁄ 共 6376字 ⁄ 字号 评论关闭

学号:sa**199  姓名:*浩

环境:ubuntu12.04  gcc4.7.3

1进程管理

         进程的一个比较正式的定义是:在自身的虚拟地址空间运行的一个单独的程序。进程与程序是有区别的,进程不是程序,虽然它由程序产生。程序只是一个静态的指令集合,不占系统的运行资源;而进程是一个随时都可能发生变化的、动态的、使用系统运行资源的程序。而且一个程序可以启动多个进程 
        系统调用system
call
),又稱為系統呼叫,指运行在使用者空間程序操作系统内核请求需要更高权限运行的服务。 系统调用提供了用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。操作系统的进程空间可分为用户空间内核空间,它们需要不同的执行权限。其中系统调用运行在内核空间。系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核核心态,而普通的库函数调用由函数库或用户自己提供,运行于用户态。

 Linux系统调用,包含了大部分常用系统调用和由系统调用派生出的的函数
 包括:进程控制,文件系统控制,文件系统操作,内存管理,网络管理,socket控制,用户管理,进程通信。
关于进程和线程的区别的文章:

 进程创建:

  fork()

   
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。所以你在子进程中修改资源的值是不会影响到父进程中的
相同资源的值的。
linux将复制父进程地址空间内容给子进程,因此,子进程有了独立的地址空间。

     为什么fork会返回两次?

由于在复制时复制了父进程堆栈段,所以两个进程都停留在fork函数中,等待返回。因为fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。过程如下图

调用fork之后,数据、堆栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
头文件:

#include<unistd.h>
#include<sys/types.h>
函数原型:
pid_t forkvoid);
(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1

我们来写一段代码来验证上文中所学到的东西:

代码:
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
 
int glob = 1;
char buf[] = "hehehehehe\n";
 
int main()
{
    int var;
    pid_t pid;
   
    var = 2;
    
    fprintf(stderr, "%s", buf);
   
    printf("before fork\n");
   
    if(( pid = fork() ) < 0 )
    {
        fprintf(stderr, "fork error\n");
    }
    else if(pid == 0)
    {
        glob++;
        var++;
        printf("child process\n");
        buf[3] = '\0';
        printf( "%s\n", buf );
        printf("pid = %d, father pid = %d, glob = %d, var = %d\n", getpid(), getppid(), glob, var);
        exit(0);
    }
    else
    {
        sleep(2);
        printf("father process\n");
        printf( "%s", buf );
        printf("pid = %d, father pid = %d, glob = %d, var = %d\n", getpid(), getppid(), glob, var);
    }
   
    return 0;
}


我们在子进程中修改了一些参数的值,来打印输出下结果:



可以看到,在子进程中修改 变量的值,并没有影响到父进程中的变量的值。OK,我们继续往下看。

例子分析

         在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。fork之后,子进程和父进程都会继续执行fork调用之后的指令。子进程是父进程的副本。它将获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段。正文段存放着由cpu执行的机器指令,通常是read-only的。

  
 这样看来,
fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程。但由于现在Linux中是采取了copy-on-write(COW写时复制)技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,因为在那个时候,大量的数据其实完全是一样的。写时复制是在推迟真正的数据拷贝。若后来确实发生了写入,那意味着parentchild的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。

fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

   了解了简单的fork()函数,我们来看一下几个经典题目来加深下对fork()的认识:下面的题目均转载自他人,我会将地址放到这。

fork 进阶题目:

1)   第一个题目:地址:http://www.cnblogs.com/leoo2sk/archive/2009/12/11/talk-about-fork-in-linux.html

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


int main()
{
	pid_t pid1;
	pid_t pid2;
	
	pid1 = fork( );
	pid2 = fork( );sleep(1);
	printf( " pid1 =  %d, pid2 = %d\n", pid1, pid2 );
}

  要求如下:

      已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行。

      1、请说出执行这个程序后,将一共运行几个进程。

      2、如果其中一个进程的输出结果是“pid1:1001, pid2:1002”,写出其他进程的输出结果(不考虑进程执行顺序)。

预备知识

      这里先列出一些必要的预备知识,对linux下进程机制比较熟悉的朋友可以略过。

      1、进程可以看做程序的一次执行过程。在linux下,每个进程有唯一的PID标识进程。PID是一个从1到32768的正整数,其中1一般是特殊进程init,其它进程从2开始依次编号。当用完32768后,从2重新开始。

      2、linux中有一个叫进程表的结构用来存储当前正在运行的进程。可以使用“ps aux”命令查看所有正在运行的进程。

      3、进程在linux中呈树状结构,init为根节点,其它进程均有父进程,某进程的父进程就是启动这个进程的进程,这个进程叫做父进程的子进程。

      4、fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

解题的关键

有了上面的预备知识,我们再来看看解题的关键。我认为,解题的关键就是要认识到fork将程序切成两段。看下图:

      上图表示一个含有fork的程序,而fork语句可以看成将程序切为A、B两个部分。然后整个程序会如下运行:

      step1、设由shell直接执行程序,生成了进程P。P执行完Part. A的所有代码。

      step2、当执行到pid = fork();时,P启动一个进程Q,Q是P的子进程,和P是同一个程序的进程。Q继承P的所有变量、环境变量、程序计数器的当前值。

      step3、在P进程中,fork()将Q的PID返回给变量pid,并继续执行Part. B的代码。

      step4、在进程Q中,将0赋给pid,并继续执行Part. B的代码。

      这里有三个点非常关键:

      1、P执行了所有程序,而Q只执行了Part. B,即fork()后面的程序。(这是因为Q继承了P的PC-程序计数器)

      2、Q继承了fork()语句执行时当前的环境,而不是程序的初始环境。

      3、P中fork()语句启动子进程Q,并将Q的PID返回,而Q中的fork()语句不启动新进程,仅将0返回。

解题

      下面利用上文阐述的知识进行解题。这里我把两个问题放在一起进行分析。

      1、从shell中执行此程序,启动了一个进程,我们设这个进程为P0,设其PID为XXX(解题过程不需知道其PID)。

      2、当执行到pid1 = fork();时,P0启动一个子进程P1,由题目知P1的PID为1001。我们暂且不管P1。

      3、P0中的fork返回1001给pid1,继续执行到pid2 = fork();,此时启动另一个新进程,设为P2,由题目知P2的PID为1002。同样暂且不管P2。

      4、P0中的第二个fork返回1002给pid2,继续执行完后续程序,结束。所以,P0的结果为“pid1:1001, pid2:1002”。

      5、再看P2,P2生成时,P0中pid1=1001,所以P2中pid1继承P0的1001,而作为子进程pid2=0。P2从第二个fork后开始执行,结束后输出“pid1:1001, pid2:0”。

      6、接着看P1,P1中第一条fork返回0给pid1,然后接着执行后面的语句。而后面接着的语句是pid2 = fork();执行到这里,P1又产生了一个新进程,设为P3。先不管P3。

      7、P1中第二条fork将P3的PID返回给pid2,由预备知识知P3的PID为1003,所以P1的pid2=1003。P1继续执行后续程序,结束,输出“pid1:0, pid2:1003”。

      8、P3作为P1的子进程,继承P1中pid1=0,并且第二条fork将0返回给pid2,所以P3最后输出“pid1:0, pid2:0”。

      9、至此,整个执行过程完毕。

      所得答案:

      1、一共执行了四个进程。(P0, P1, P2, P3)

      2、另外几个进程的输出分别为:

      pid1:1001, pid2:0

      pid1:0, pid2:1003

      pid1:0, pid2:0

      进一步可以给出一个以P0为根的进程树:


验证

这个在我的电脑下跑出来的结果是:


因为电脑分配的ID,不可能正好是1001 ,10002 ,所以这个结果也是正常的。根据我们的分析。


OK,我们加大点难度 ,看下带for循环的。这个会有点晕的。

fork 函数进阶2:代码和分析全部来自:http://coolshell.cn/articles/7965.html  (大牛的博客)可以收获很多。

代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
	  pid_t pid;
	  int i;
	  for(i=0; i<2; i++){
		 pid = fork();
		 printf( " ppid = %d ,pid = %d ,i = %d \n", getppid(),getpid(),i );
		
	  }
	  sleep(10); //查看下进程树
	  return 0;
}


问题是:会打印几行,总共有几个进程在运行。

看看打印结果:


总共有4个进程在运行.看下 博主的分析。

于是,上面这段程序会输出下面的结果,(注:编译出的可执行的程序名为fork)

1
2
3
4
5
6
7
8
9
10
ppid=8858,
pid=8518, i=0
ppid=8858,
pid=8518, i=1
ppid=8518,
pid=8519, i=0
ppid=8518,
pid=8519, i=1
ppid=8518,
pid=8520, i=1
ppid=8519,
pid=8521, i=1
 
$
pstree -p |
grepfork
|-bash(8858)-+-fork(8518)-+-fork(8519)---fork(8521)
|           
|            `-fork(8520)

面对这样的图你可能还是看不懂,没事,我好事做到底,画个图给你看看:

注意:上图中的我用了几个色彩,相同颜色的是同一个进程。于是,我们的pstree的图示就可以成为下面这个样子:(下图中的颜色与上图对应)

OK 生成了3个子进程,打印运行了 6次。OK,理解 了。
我们继续深入一下 ,看下面的代码会打印输出什么东西:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("-");
      //fflush(stdout);
   }
   
   return 0;
}


问题是会打印几个 -  ,经过刚才的分析我们知道 会打印 6次,所以应该是打印 6个  -  ,答案是错误的,会打印8个。。。 问题原因在哪里,看下面的分析。


我们首先需要知道fork()系统调用的特性,

  • fork()系统调用是Unix下以自身进程创建子进程的系统调用,一次调用,两次返回,如果返回是0,则是子进程,如果返回值>0,则是父进程(返回值是子进程的pid),这是众为周知的。
  • 还有一个很重要的东西是,在fork()的调用处,整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区,等等。

所以,上面的那个程序为什么会输入8个“-”,这是因为printf(“-”);语句有buffer,所以,对于上述程序,printf(“-”);把“-”放到了缓存中,并没有真正的输出(参看《C语言的迷题》中的第一题),在fork的时候,缓存被复制到了子进程空间,所以,就多了两个,就成了8个,而不是6个。

另外,多说一下,我们知道,Unix下的设备有“块设备”和“字符设备”的概念,所谓块设备,就是以一块一块的数据存取的设备,字符设备是一次存取一个字符的设备。磁盘、内存都是块设备,字符设备如键盘和串口。块设备一般都有缓存,而字符设备一般都没有缓存

对于上面的问题,我们如果修改一下上面的printf的那条语句为:

抱歉!评论已关闭.