linux系统编程之进程(一)
今天起,开始学习linux系统编程中的另一个新的知识点----进程,在学习进程之前,有很多关于进程的概念需要了解,但是,概念是很枯燥的,也是让人很容易迷糊的,所以,先抛开这些抽象的概念,以实际编码来熟悉进程到底是个什么东东,这样学习起来要有兴趣一些,在学习一门技术时,找一种能提高自己兴趣的方法是很重要的,这也是我自己学习的一个比较"典型"的学习流程吧,关于不太清楚的概念会在实验中一一阐述(当然不可能理解得很透,但是没关系,先把实践做出来,随时间的推移再慢慢摸透它),理论当然也是非常非常重要的,等到进程学得差不多了,我会再回过头来去系统地分析理论,实践支配理论,这是非常重要的学习方法,好了,话不多说,进入正题!
复制进程映像:
【说明:利用fork复制的新进程,并未全部都复制了父进程的东东,这个在结尾时还会说明,先有个了解】
关于fork进程,可以用下面这种的通俗方式来理解:
首先我们先来理解一个概念,这个之后在最后还会进行总结的:
其中PCB是Process Control Block(进程控制块),对于这些理论,先有个大至的了解,之后会进程中所有的概念进行一个总结的。
理解上面的东东之后,fork新创建的进程,会复制父进程的所有信息(代码段+数据段+堆栈段+PCB),但是“所有”并非绝对,还是有少部分信息是不一样的,另外还可以理解,每一个进程都有自己独立的4GB的地址空间(对于32位系统来说)
子进程与父进程的区别在于:
1、父进程设置的锁,子进程不继承
对于排它锁(关于什么是排它锁可以参见博文:http://www.cnblogs.com/webor2006/p/3500354.html),如果说子进程会共享父进程的锁的话,那就有矛盾了。
2、各自的进程ID和父进程ID不同
3、子进程的未决告警被清除【了解】
4、子进程的未决信号集设置为空集【了解】
fork系统调用:
如上图所说:fork()出来的子进程成功,对于子进程来说则返回0;而对于父进程来说返回子进程ID,这是有原因的,对于进程中的PCB保存了pid和ppid,所以对于子进程来说,有办法知道pid和ppid,而对于父进程来说,如果不返回子进程的id,则就无法知道新创建的进程的号码,因为PCB中并不会保存子进程的ID列表,这样就会让PCB膨胀,所以这也是有原因的。
有了上面的理论之后,下面就以实际代码来进行说明:
#include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <signal.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(int argc, char *argv[]) { printf("before fork pid = %d\n", getpid());//打印当前进程ID,也就是原始父进程 pid_t pid; pid = fork();//产生一个新的进程,注意:它里会有两个进程执行 if (pid == -1) ERR_EXIT("fork error"); if (pid > 0) {//证明是父进程 printf("this is parent pid=%d childpid=%d\n", getpid(), pid);//getpid()为当前父进程的ID,而pid则为新创建的进程id,也就是子进程 } else if (pid == 0) {//证明是子进程 printf("this is child pid=%d parentpid=%d\n", getpid(), getppid());//getpid()为当前子进程的ID,getppid()为当前子进程所属父进程的ID } return 0; }
编译运行:
原因是由于当执行父进程之后,它就退出了,这时子进程执行时,这时子进程的父进程就变为init进程,所以就变成了1,如果我们让父进程输出延时一下,保证子进程执行时父进程没退出,就如我们的预期了:
这次再看效果:
对于fork函数,可能有一点比较难以理解,为啥它一次调用会有两次返回呢?这里再来用文字来解释一下:fork成功意味着创建了一个进程副本,意味着也就有两个进程了,两个进程都要执行各自相应的动作,所以两个进程都得要返回,实际上在内核中,是在各自的进程地址空间中返回的:
fork 系统调用注意点:
关于上面的第二个注意点,,如果父进程退出了,子进程还没有退出,我们将子进程称为孤儿进程,这时会将子进程托孤给init进程。
关于第三点,其中提到了“僵尸进程”,用程序来看下现象:
运行:
这时,查看一下当前的进程状态:
僵尸状态,我们尽量得避免它,避免它的方法之一,可以采用一个系统调用----signal(信号,关于它,之后会详述,这里只是先了解一下):
这时,编译运行,再看效果:
下面再来理解一来系统是如何实现fork()的:
写时复制copy on write:
实际系统实现时,并未真正把所有的数据(代码段+数据段+堆栈段+PCB)都复制一份,只是为了方便理解,我们可以认为是数据(代码段+数据段+堆栈段+PCB)都复制了一份,实际上代码段是只读的,是可以被共享的,每个进程只要保存一个指向这个资源的指针既可,这可以加快进程的创建速度,大大提高了效率
而对于需要修改的进程才会复制那份资源,对于linux而言,它是基于页的的方式进行复制的,并没将所有数据都进行复制,只是复制需要页,其它页是不会复制的,所以我们得正确理解“每个进程有自己独立的4GB(对于32位系统来说)的地址空间”,实际上不被修改的数据是共享的,对于这个理论,大致了解下,也是为了加深对fork()函数的理解。
好了,今天就学到这,下回见!