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!

 

我们可以看到,程序并没有打印后面的"never be displayed!\n",因为在此之前,在执行到exit(0)时,进程就已经终止了。
exit 系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;其他的数值表示出现了错误,进程非正常结束。我们在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。关于wait的详细情况,我们将在以后的篇幅中进行介绍。
 
exit和_exit
作为系统调用而言,_exit和exit是一对孪生兄弟。
这时随便一个懂得C语言并且头脑清醒的人都会说,_exit和exit没有任何区别,但我们还要讲一下这两者之间的区别,这种区别主要体现在它们在函数库中
的定义。_exit在Linux函数库中的原型是:
#include<unistd.h>
void _exit(int status);

 

exit比较一下,exit()函数定义在 stdlib.h中,而_exit()定义在unistd.h中,从名字上看,stdlib.h似乎比unistd.h高级一点,那么,它们之间到底有什么区别呢?
 
_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是“清理I/O缓冲”。
在Linux 的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数
据的完整性,就一定要使用exit()函数。
请看以下例程
/* 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的大多数服务器都是用守护进程实现的。

 

 

Linux大多数服务都是通过守护进程实现的,完成许多系统任务
0: 调度进程,称为交换进程(swapper),内核一部分,系统进程
1: init进程, 内核调用,负责内核启动后启动Linux系统
没有终端限制
让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程

 

 

2、C实现Linux守护进程步骤

 


1. 创建子进程,父进程退出
所有工作在子进程中进行
形式上脱离了控制终端
2. 在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制
3. 改变当前目录为根目录
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
4. 重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
5. 关闭文件描述符
继承的打开文件不会用到,浪费系统资源,无法卸载
getdtablesize()
返回所在进程的文件描述符表的项数,即该进程打开的文件数目

 

 

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,说明该程序一直在运行

 

微笑四.进程组,会话

 

进程组
一个或多个进程的集合
进程组ID: 正整数
两个函数
getpgid(0)=getpgrp()

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,即父进程为组长进程

 

组长进程
组长进程标识: 其进程组ID==其进程ID
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止
只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
进程组生存期: 进程组创建到最后一个进程离开(终止或转移到另一个进程组)
 
一个进程可以为自己或子进程设置进程组ID
setpgid()加入一个现有的进程组或创建一个新进程组

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;
 }

 

 

会话: 一个或多个进程组的集合
开始于用户登录
终止与用户退出
此期间所有进程都属于这个会话期

建立新会话:setsid()函数
该调用进程是组长进程,则出错返回
先调用fork, 父进程终止,子进程调用
该调用进程不是组长进程,则创建一个新会话
•该进程变成新会话首进程(session header)
•该进程成为一个新进程组的组长进程。
•该进程没有控制终端,如果之前有,则会被中断
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程...

会话ID:会话首进程的进程组ID
获取会话ID: getsid()函数

#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


posted @ 2013-06-14 23:44  爱生活,爱编程  阅读(250)  评论(0编辑  收藏  举报