Linux进程,fork-专研精讲(实例讲解)!!!


创建进程有两种方式,1:由操作系统创建;2:由父进程创建

由操作系统创建的进程,它们之间是平等的,一般不存在资源继承关系(称之为:系统进程)。而对于由父进程创建的进程(子进程),它们和父进程之间是隶属的关系,然后子进程继续创建属于自己的子进程,形成进程家族,子进程可以继承其父进程几乎所有资源


系统调用fork是创建一个新进程的唯一方法


pid_t(类型探究)(参考博客:http://blog.chinaunix.net/uid-20753645-id-1877915.html)

1,/usr/include/sys/types.h中有如下定义

#ifndef __pid_t_defined
typedef __pid_t pid_t;
# define __pid_t_defined
#endif
pid_t 其实就是__pid_t类型) 

2,/usr/include/bits/types.h中可以看到这样的定义

/* We want __extension__ before typedef's that use nonstandard base types
   such as `long long' in C89 mode.  */
# define __STD_TYPE             __extension__ typedef
#elif __WORDSIZE == 64
# define __SQUAD_TYPE           long int
# define __UQUAD_TYPE           unsigned long int

....................
 __STD_TYPE __PID_T_TYPE __pid_t;        /* Type of process identifications.  */
__STD_TYPE __FSID_T_TYPE __fsid_t;      /* Type of file system IDs.  */
__STD_TYPE __CLOCK_T_TYPE __clock_t;    /* Type of CPU usage counts.  */
__STD_TYPE __RLIM_T_TYPE __rlim_t;      /* Type for resource measurement.  */
这里我们要注意的是:__extension__ typedef(关于__extension__的作用:gcc对标准C语言进行了扩展,但用到这些扩展功能时,编译器会提出警告,使用__extension__关键字会告诉gcc不要提出警告,所以说:相当于typedef)

(__PID_T_TYPE也就是__pid_t)

3,/usr/include/bits/typesizes.h中可以看到这样的定义

#define __OFF64_T_TYPE          __SQUAD_TYPE
#define __PID_T_TYPE            __S32_TYPE
#define __RLIM_T_TYPE           __SYSCALL_ULONG_TYPE
(_S32_TYPE也就是__PID_T_TYPE)

4,/usr/include/bits/types.h中我们终于找到了这样的定义

#define __U16_TYPE              unsigned short int
#define __S32_TYPE              int
#define __U32_TYPE              unsigned int
到了这里,我们终于找到了定义,原来:pid_t就是int类型的了


pid: 调用fork函数的返回值,(0:子进程的运行)(-1:进程创建失败)(其他:一般为子进程的标识符)

getpid:(当前进程的标识符)

getpid() returns the process ID of the calling process.  (This is often used by routines that generate unique temporary filenames.)


getppid:(当前进程的父进程的标识符)

getppid() returns the process ID of the parent of the calling process.

程序1:

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

int main()
{
        pid_t pid;

        printf(" %d   %d   %d\n", pid,getpid(), getppid());
        printf("Process Creation Study!\n");
        pid = fork();
        switch(pid)
        {
                case 0:
                        printf("Child process is running , retuernpid is %d,curentpid is %d, Parent is %d\n",pid, getpid(), getppid());
                        break;
                case -1:
                        printf("Process creation failed\n");
                        break; 
                default:
                        printf("Parent process is running , returnpid is %d,curentpid is %d, parentpid is %d\n",pid ,getpid(), getppid());
        }

        exit(0);
}

运行结果:


从运行结果可以看到:

PID:0      getpid():5581    getppid():5557

说明:当前这个程序(main函数)调入内存变成进程,应该是由其它的进程(进程号为5557的那个进程,应该是调用fork函数,产生的子进程(进程号为5581),fork返回为0)

这里查看一下进程:ps -A

Linux进程,fork-专研精讲(实例讲解)!!!_第1张图片

。。。。。

可以知道那个进程号为5557的进程就是bash(一般来说,安装linux时,如果没有改shell,默认的都是bash,所以我们开启一个终端就会生成一个叫做bash的进程,再打开一个终端又会生成一个bash进程,关掉一个终端就会少一个bash进程,但是终端并不是bash,终端是一个界面,但它调用bash)

这里我们来看一下进程树:pstree

我现在开了四个终端


接着看:

下面程序执行了:

父进程:returnpid:5582    current:5581    getppid:5557

(主函数中的fork返回5582,当前的进程号为5581,父进程为5557,也就是上面提到的bash进程)

子进程:returnpid:0          current:5582    getppid:5581

(fork函数返回0,当前是子进程在执行,进程号为5582,父进程为5581)



关于执行顺序的探究:

程序2:

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

int main()
{
        pid_t pid;
        char *msg = NULL;
        int k;
        
        printf("process creation study\n");
        pid = fork();
        switch(pid)
        {
                case 0:
                        msg = "child process is running";
                        k = 3;
                        break;
                case -1:
                        perror("process creation failed\n");
                        break;
                default:
                        msg = "parent process is running";
                        k = 5;
                        break;  
        }

        while(k > 0)
        {
                puts(msg);
                sleep(1);
                k--;    
        }

        exit(0);
} 
运行结果:

Linux进程,fork-专研精讲(实例讲解)!!!_第2张图片

这是没有调用sleep的程序

  while(k > 0)
        {
                puts(msg);
//              sleep(1);   //这是去掉sleep的情况
                k--;
        }

运行结果:

可以很容易的看到,只有在父进程彻底执行完之后才会执行子进程


然后我们很容易很自然的总结吗???

每次一开始,对于我当前的系统来说:总是父进程先执行的,那么是不是说明:内核所使用的调度算法中,我的父进程优先于子进程呢??

当输出多条语句的话,总是父进程先执行完之后,子进程才会执行的,如果存在sleep的话,相当于中断,将当前系统的控制权交出去了,而子进程此刻也在等待着,所以子进程执行,同理,父进程执行,这样形成的交替吗???



上面这些真的对吗,其实,一开始我真的是这样想的,哈哈,但是经过验证之后,并不是这样的

1,对于上面的程序而言(去掉sleep语句),当多运行几次之后,我们会发现,并不是所有的运行结果都是和上面所  

      想的一样,有的运行结果是父进程还没有执行完,子进程就开始执行了,所以说:上面的解释不同

2,对于一开始,父进程总是先执行,这也是有原因的:因为对于进程的创建而言,这是特别麻烦的一件事(给一块存

      储空间,复制父进程的大部分资源,代码段,数据段,创建id号,等等)形成子进程,我们不要忘了,子进程的形

      成需要很多的时间,所以在这个时间里父进程开始执行的,所以我们总是会直观的看到父进程总是先执行的

3,那么如何理解:一般来说:fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法,这句话的呢???

      其实,这句话是完全正确的,只是我们曲解了它的意思,它的意思是在父子进程彻底,完全的创建之后,父进程还是子进程的执行顺序是不确定的,上面没有sleep,父进程先打印完自己的五条语句,说明了:打印完5条语句特别快,比子进程彻底创建完成还要快,所以父进程的先打印完成

      当时对于很多条的打印,我们可以看看:

     

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

int main()
{
        pid_t pid;
        char *msg = NULL;
        int k;

        printf("process creation study\n");
        pid = fork();
        switch(pid)
        {
                case 0:
                        msg = "child process is running";
                        k = 100;
                        break;
                case -1:
                        perror("process creation failed\n");
                        break;
                default:
                        msg = "parent process is running";
                        k = 100;
                        break;
        }

        while(k > 0)
        {
                puts(msg);
                k--;
        }

        exit(0);
}
         可以看到,程序中,没有sleep语句,只是这时候,我们将程序中的循环变量k改为了100:

         运行结果:

         Linux进程,fork-专研精讲(实例讲解)!!!_第3张图片

         这个东西只是我们的部分截图,但是也可以很明显的看到父子进程在交替运行,也就是在抢占资源了





程序3(printf输出问题)

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

int main()
{
        printf("today is good!");
        pid_t pid;
        pid = fork();
        if(pid == 0)
        {
                printf("child process is running\n");
        }
        else if(pid != -1)
        {
                printf("parent process is running\n");
        }

        return 0;
}
运行结果:

当程序为这样时:

printf("today is good!\n");


程序的唯一区别就是:printf的输出有没有(\n)换行符,两者为什么输出的差距这么大呢,一个输出一次,一个输出两次

后来查了查,这个跟printf的缓冲机制有关(LINUX和Window下的不一样),linux下:printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列中,并没有实际的写到屏幕上,但是,只要满足printf的刷新条件(缓冲区填满

,写入的字符中有‘\n’, '\r', '\t',调用fflush手动刷新缓冲区,调用scanf要从缓冲区中读取数据时),就会立即刷新,马上就会打印了,对于当前的程序而言:

运行了printf(“today is good!”),字符串内容被放到了缓冲里,程序运行到fork时,缓冲里的内容也被子进程复制过去了。因此在子进程stdout缓存里面也就有了相应的字符串,所以我们会看到最终的被打印了两遍!!!

而一旦后面加上了换行,那么缓冲里面的字符串直接就被刷新了,就不会有相应的字符被复制到子进程了的!!!



那么从fork之后,父子进程到底共享了那些东西?设置全局变量,静态变量,局部变量到底会不会在父子进程中改变呢???

程序3(探究全局变量,静态变量,局部变量)

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

int a = 0;           //全局变量,判断共享
static int b = 0;    //静态全局变量

int main()
{
        int c = 0;   //局部变量                                                                                                                 
        pid_t pid;
        pid = fork();

        if(pid == 0)
        {
                printf("child process is running\n");
                a = a + 2;   b = b + 2;
                c = c + 4;
        }
        else if(pid != -1)
        {
                printf("parent process is running\n");
                a = a + 3;  b = b + 3;
                c = c + 5;
        }

        printf("全局变量a is :%d\t   静态变量b is :%d\n", a, b);
        printf("局部变量c is :%d\n", c);
        return 0;
}
运行结果:

Linux进程,fork-专研精讲(实例讲解)!!!_第4张图片

可以很容易看到对于全局变量,静态变量,局部变量,在父子进程并不是共享的

关于其他继承的关系:man fork命令

fork() creates a new process by duplicating the calling process.  The new process, referred to as the child, is an exact duplicate of
       the calling process, referred to as the parent, except for the following points:

       *  The child has its own unique process ID, and this PID does not match the ID of any existing process group (setpgid(2)).

       *  The child's parent process ID is the same as the parent's process ID.

       *  The child does not inherit its parent's memory locks (mlock(2), mlockall(2)).

       *  Process resource utilizations (getrusage(2)) and CPU time counters (times(2)) are reset to zero in the child.

       *  The child's set of pending signals is initially empty (sigpending(2)).

       *  The child does not inherit semaphore adjustments from its parent (semop(2)).

       *  The child does not inherit record locks from its parent (fcntl(2)).

       *  The child does not inherit timers from its parent (setitimer(2), alarm(2), timer_create(2)).

       *  The child does not inherit outstanding asynchronous I/O operations from  its  parent  (aio_read(3),  aio_write(3)),  nor  does  it
          inherit any asynchronous I/O contexts from its parent (see io_setup(2)).

       The process attributes in the preceding list are all specified in POSIX.1-2001.  The parent and child also differ with respect to the
       following Linux-specific process attributes:
       *  The child does not inherit directory change notifications (dnotify) from its parent (see the description of F_NOTIFY in fcntl(2)).

       *  The prctl(2) PR_SET_PDEATHSIG setting is reset so that the child does not receive a signal when its parent terminates.

       *  The default timer slack value is set to the parent's current timer slack value.   See  the  description  of  PR_SET_TIMERSLACK  in
          prctl(2).

       *  Memory mappings that have been marked with the madvise(2) MADV_DONTFORK flag are not inherited across a fork().

       *  The termination signal of the child is always SIGCHLD (see clone(2)).

       *  The  port access permission bits set by ioperm(2) are not inherited by the child; the child must turn on any bits that it requires
          using ioperm(2).

       Note the following further points:

       *  The child process is created with a single thread—the one that called fork().  The entire virtual address space of the  parent  is
          replicated  in  the  child,  including  the  states  of  mutexes,  condition  variables,  and  other  pthreads objects; the use of
          pthread_atfork(3) may be helpful for dealing with problems that this can cause.

       *  The child inherits copies of the parent's set of open file descriptors.  Each file descriptor in the child refers to the same open
          file description (see open(2)) as the corresponding file descriptor in the parent.  This means that the two descriptors share open
          file status flags, current file offset, and signal-driven I/O  attributes  (see  the  description  of  F_SETOWN  and  F_SETSIG  in
          fcntl(2)).

       *  The  child  inherits  copies  of  the parent's set of open message queue descriptors (see mq_overview(7)).  Each descriptor in the
          child refers to the same open message queue description as the corresponding descriptor in the parent.  This means  that  the  two
          descriptors share the same flags (mq_flags).

       *  The  child inherits copies of the parent's set of open directory streams (see opendir(3)).  POSIX.1-2001 says that the correspond‐
          ing directory streams in the parent and child may share the directory stream positioning; on Linux/glibc they do not. 


程序4(文件指针的探究):

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

int main()
{
        printf("today is good\n");
        pid_t pid;
        pid = fork();
        printf("tomarrow is also good\n");
        if(pid == 0)
        {
                printf("child process is running\n");
        }
        else if(pid != -1)
        {
                printf("parent process is running\n");
        }

        return 0;
}
运行结果:

Linux进程,fork-专研精讲(实例讲解)!!!_第5张图片

可以看到:today is good打印了一遍,tomorrow is also good语句打印了两遍,说明子进程基本上复制了父进程,包括文件指针,但是文件指针指向了fork函数那个位置(所以today只执行一遍),所以两个进程都从那个位置开始执行,所以才会将tomorrow打印两遍啊


程序5:(关于孤儿进程)

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


int main()
{
        pid_t pid;

        pid = fork();
        switch(pid)
        {
                case 0:
                        printf("child process is running,  getpid :%d  getppid:%d\n", getpid(), getppid());
                        sleep(1);
                        printf("child process is running,  getpid :%d  getppid:%d\n", getpid(), getppid());
                        break;
                case -1:
                        perror("process creation failed\n");
                        exit(-1);
                default:
                        printf("parent process is running , getpid:%d  getppid:%d\n", getpid(), getppid());
                        exit(0);
        }
        return 0;
}
运行结果:

可以看到:对于子进程的两次打印来看,第一次的时候,说明当前这个进程是有父进程的,而sleep完之后,再次打印,说明这个进程已经没有父进程了,是个孤儿进程了,有人可能会问???我知道,是init这个进程,但是它的id号不是1吗??这里的是1803,所以应该不是孤儿进程,,,,

是吗???,这里我们可以再来看看

ps  -A(命令)



哈哈,这回没有问题了吧!!!


当然,fork也可能会出错:

创建进程失败返回-1:

1,父进程拥有的子进程的个数超过了规定的限制,此时errno值为:EAGAIN

2,可供使用的内存不足导致进程创建失败,此时errno值为:ENOMEM


程序4:连续调用fork函数

#include <unistd.h>
#include <stdio.h>

int main()
{
	int i = 0;
	pid_t pid;
	for(i = 0; i < 2 ; i++)
	{
		pid = fork();
		if(pid == 0)
			printf("%d  child   pid:%d  getpid:%d  getppid:%d\n" ,i , pid, getpid(), getppid());
		else if(pid == -1)
			printf("the create process is fail\n");	
		else
			
			printf("%d  parent  pid:%d  getpid:%d  getppid:%d\n" ,i , pid, getpid(), getppid());
	}
}
运行结果:

Linux进程,fork-专研精讲(实例讲解)!!!_第6张图片

我们来仔细分析一下:

当前的程序,我们称之为main进程

1,首先在for循环中,i = 0;第一次执行fork(这里会产生一个子进程)

      输出结果(第一行):parent,pid为3750,说明此刻执行的是fork之后的父进程(main进程),3750为新产生的子

      进程的pid,当前main进程的pid为3749,3052为main进程的父进程(bash进程)

2,for循环中,i = 1;第二次执行fork(这里还是会产生一个子进程)

      输出结果(第二行):同上,此刻再次新产生了一个子进程, pid = 3751

3,输出结果(第三行):0,  child,pid:0,(getpid:3750)说明这个是第一次fork执行时产生的子进程,

      getppid:3749,也就是 说:它是main进程的第一个子进程

4,输出结果(第四行):1,  child,pid:0,(getpid:3751)说明这个是第二次fork执行时产生的子进程,

      getppid:3749,也就是说:它是main进程的第二个子进程

5,这是第三次执行fork函数(是第一次产生的fork子进程,进入for循环后,i = 1,调用fork的结果)

      输出结果(第五行):1, parent,pid:3752,(getpid:3750),说明当前的进程是main进程产生的第一个进

      程,然后,产生了一个新的进程,进程号为:3752

6,这是第三次执行fork,返回的子进程的内容

      输出结果(第六行):1, child   , pid:0,(getpid:3752),是第三次fork后的子进程,也是main进程产生的子进程的子进程


大家可能会注意到,上面的程序中

第三次fork之后,也就是main进程的子进程调用fork后

对于当前进程(main进程的子进程),它的getppid:为1803,可能会想不通,这是因为,此刻main进程已经结束了,当前这个进程已经变成了孤儿进程了

草图:

Linux进程,fork-专研精讲(实例讲解)!!!_第7张图片

或者我能还能发现创建进程pid就如同链表一般:3052--》3749--》3750--》3751--》3752


int main()
{
	int i = 0;
	pid_t pid;
	for(i = 0; i < 3; i++)
	{
		pid = fork();
		if(pid == 0)
			printf("son\n");
		else
			printf("father\n");
	}	
	return 0;
}
运行结果:

Linux进程,fork-专研精讲(实例讲解)!!!_第8张图片

所以,是这样子的:

对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1



最后,我们再来看看最后一个程序(问总共多少个进程):

#include <unistd.h>
#include <stdio.h>

int main()
{
        fork();
        fork() && fork() || fork();
        fork();
        
        return 0;                                                                                                                               
}

1,首先注意到的是   :  &&   ||

      A && B:如果A为0的话,那么就不会探求B的真假了

      A || B:如果A为1,就没有必要执行后面的了,因为整条语句的真假,我们已经知道了

      这里主要的意思是:根据fork的返回值来决定后续的fork是否执行

      

      恩,对,如果不算main进程的话,应该是15个,总共是16个,主要的也就是清楚当前的fork应不应该执行!!!


如有问题:欢迎留言!!!

你可能感兴趣的:(孤儿进程,fork父子进程执行顺序,bash进程fork,父子进程中的变量问题,pid_t探究)