Linux System Programming --Chapter Five
这一章中的内容出现在博主的多篇文章中,所以并不对这一章进行详细的说明解释,只是对几个比较重要的概念进行说明
一.写时复制技术
COW技术初窥:
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
COW详述:
现在有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。
1. 现在P1用fork()函数为进程创建一个子进程P2,
内核:
(1)复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
(2)为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。
2. 写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
3. vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间
通过以上的分析,相信大家对进程有个深入的认识,它是怎么一层层体现出自己来的,进程是一个主体,那么它就有灵魂与身体,系统必须为实现它创建相应的实体, 灵魂实体与物理实体。这两者在系统中都有相应的数据结构表示,物理实体更是体现了它的物理意义。以下援引LKD
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW与exec没有必然联系
二.exit()和 _exit()的不同
#include<stdlib.h> void exit(int status);
不像fork那么难理解,从exit的名字就能看出,这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。请看下面的程序:
/* exit_test1.c */ #include<stdlib.h> main() www.2cto.com { printf("this process will exit!\n"); exit(0); printf("never be displayed!\n"); }
编译后运行:
$gcc exit_test1.c -o exit_test1 $./exit_test1 this process will exit!
#include<unistd.h> void _exit(int status);
/* exit2.c */ #include<stdlib.h> main() { printf("output begin\n"); printf("content in buffer"); exit(0); }
编译并运行:
$gcc exit2.c -o exit2 $./exit2 output begin content in buffer
/* _exit1.c */ #include<unistd.h> main() www.2cto.com { printf("output begin\n"); printf("content in buffer"); _exit(0); }
编译并运行:
$gcc _exit1.c -o _exit1 $./_exit1 output begin
三.守护进程
1、概念
守护进程是运行在后台,并且一直在运行的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些事。
Linux的大多数服务器都是用守护进程实现的。
2、C实现Linux守护进程步骤
3、案例
守护进程每10秒钟向/tmp/log.txt中写入日至。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <signal.h> #include <sys/param.h> #include <sys/stat.h> void init_daemon(void); int main(void){ FILE *fp; time_t t; init_daemon(); while(1){ sleep(10); if( (fp=fopen("log.txt", "a+")) >= 0){//打开log文件,若没有此文件则创建 t=time(0); fprintf(fp, "守护进程正在运行,时间是:%s", asctime(localtime(&t))); fclose(fp); } } return 1; } void init_daemon(void){ pid_t pid; int i; pid = fork(); if(pid > 0){ //终止父进程 exit(0); }else if(pid < 0 ){ perror("创建子进程失败\n"); exit(1); }else if(pid == 0){ setsid();//子进程中创建新会话 chdir("/tmp");//改变工作目录到tmp umask(0); //重设文件掩码 for(i=0; i<NOFILE;++i){//关闭文件描述符 close(i); } return ; } }
pid_t setsid(void);
设置新的组进程号
成功返回进程组号GID,失败返回-1,原因存于errno。
案例2
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #include <fcntl.h> int main() { pid_t pid; int i,fd; char *buf="This is a daemon program.\n"; if ((pid=fork())<0) { printf("fork error!"); exit(1); }else if (pid>0) // fork且退出父进程 exit(0); setsid(); // 在子进程中创建新会话。 chdir("/"); // 设置工作目录为根 umask(0); // 设置权限掩码 for(i=0;i<getdtablesize();i++) //getdtablesize返回子进程文件描述符表的项数 close(i); // 关闭这些不将用到的文件描述符 while(1) {// 死循环表征它将一直运行 // 以读写方式打开"/tmp/daemon.log",返回的文件描述符赋给fd if ((fd=open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0) { printf("Open file error!\n"); exit(1); } // 将buf写到fd中 write(fd,buf,strlen(buf)+1); close(fd); sleep(10); printf("Never output!\n"); } return 0; }
因为stdout被关掉了,所以“Never ouput!”不会输出。
查看/tmp/daemon.log,说明该程序一直在运行
四.进程组,会话
eg:显示子进程与父进程的进程组id
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid;
if ((pid=fork())<0) {
printf("fork error!");
}else if (pid==0) {
printf("The child process PID is %d.\n",getpid());
printf("The Group ID is %d.\n",getpgrp());
printf("The Group ID is %d.\n",getpgid(0));
printf("The Group ID is %d.\n",getpgid(getpid()));
exit(0);
}
sleep(3);
printf("The parent process PID is %d.\n",getpid());
printf("The Group ID is %d.\n",getpgrp());
return 0;
}
进程组id = 父进程id,即父进程为组长进程
eg:父进程改变自身和子进程的组id
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid;
if ((pid=fork())<0) {
printf("fork error!");
exit(1);
}else if (pid==0) {
printf("The child process PID is %d.\n",getpid());
printf("The Group ID of child is %d.\n",getpgid(0)); // 返回组id
sleep(5);
printf("The Group ID of child is changed to %d.\n",getpgid(0));
exit(0);
}
sleep(1);
setpgid(pid,pid); // 改变子进程的组id为子进程本身
sleep(5);
printf("The parent process PID is %d.\n",getpid());
printf("The parent of parent process PID is %d.\n",getppid());
printf("The Group ID of parent is %d.\n",getpgid(0));
setpgid(getpid(),getppid()); // 改变父进程的组id为父进程的父进程
printf("The Group ID of parent is changed to %d.\n",getpgid(0));
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid;
if ((pid=fork())<0) {
printf("fork error!");
exit(1);
}else if (pid==0) {
printf("The child process PID is %d.\n",getpid());
printf("The Group ID of child is %d.\n",getpgid(0));
printf("The Session ID of child is %d.\n",getsid(0));
sleep(10);
setsid(); // 子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程
printf("Changed:\n");
printf("The child process PID is %d.\n",getpid());
printf("The Group ID of child is %d.\n",getpgid(0));
printf("The Session ID of child is %d.\n",getsid(0));
sleep(20);
exit(0);
}
return 0;
}
在子进程中调用setsid()后,子进程成为新会话首进程,且成为一个组长进程,其进程组id等于会话id