这一章中的内容出现在博主的多篇文章中,所以并不对这一章进行详细的说明解释,只是对几个比较重要的概念进行说明

一.写时复制技术

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");//改变工作目录到tmpumask(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",返回的文件描述符赋给fdif ((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)); // 返回组idsleep(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

Linux System Programming --Chapter Five相关推荐

  1. Linux System Programming --Chapter Nine

    这一章的标题是 "信号" ,所以本文将对信号的各个方面进行介绍,由于Linux中的信号机制远比想象的要复杂,所以,本文不会讲的很全面... 信号机制是进程之间相互传递消息的一种方法 ...

  2. Linux System Programming --Chapter Two

    首先,需要解释的东西是Linux系统调用 一. 什么是系统调用      在Linux的世界里,我们经常会遇到系统调用这一术语,所谓系统调用,就是内核提供的.功能十分强大的一系列的函数.这些系统调用是 ...

  3. Linux System Programming --Chapter Eight

    内存管理 一.分配动态内存的几个函数 用户空间内存分配:malloc.calloc.realloc 1.malloc原型如下: extern void *malloc(unsigned int num ...

  4. Linux System Programming --Chapter Seven

    文件和目录管理 一.文件与其元数据 我们首先看一下一个简单的文本文件是怎么保存的: 打开vim,编辑一段文本: [root@localhost ~]# vim hello.txt 编辑内容如下: op ...

  5. Linux System Programming --Chapter Six

    这一章的题目是--高级进程管理,这篇文章将以书中所叙的顺序进行讲解 1.让出处理器 Linux提供一个系统调用运行进程主动让出执行权:sched_yield.进程运行的好好的,为什么需要这个函数呢?有 ...

  6. Linux System Programming --Chapter Four

    这一章介绍的主题是--高级文件 I/O 一. 分散--聚集I/O 分散聚集I/O是一种进行输入和输出的方法.通过此方法,单一系统调用可以将缓冲区向量写入单一数据流,或者将单一数据流读取到缓冲区向量.这 ...

  7. Linux System Programming --Chapter Three

    这一章的主题是--缓冲式I/O 一,流与缓冲        流I/O是由C语言的标准函数提供的,这些I/O可以替代系统中提供的read和write函数.事实上流I/O的内 部封装了这两个基本的文件读写 ...

  8. 书评:Linux System Programming

    原文地址:http://books.slashdot.org/article.pl?no_d2=1&sid=08/04/14/1415215 作者:Jon Mitchell "本文略 ...

  9. Linux System Programming -- Appendix

    这本书附录的名字是 "GCC对C语言的扩展" ,一下的内容是对扩展的总结 类型发现 GCC 允许通过变量的引用识别类型.这种操作支持泛型编程.在 C++.Ada 和 Java™ 语 ...

最新文章

  1. Makefile (2) gdb
  2. 解决 IntelliJ IDEA 安装后界面消失,再次打开后界面不动
  3. 【Pytorch神经网络实战案例】03 CIFAR-10数据集:Pytorch使用GPU训练CNN模版-测试方法
  4. 局域网共享设置——权限问题
  5. ORACLE 常用函数——日期/时间函数
  6. mapxtreme java_MapXtreme Java
  7. SVG 与 Canvas:如何选择
  8. 吉吉影音官网版 v2.7.2.8_p2p播放器​
  9. lintcode--报数
  10. CRT、CER、PEM、DER编码、JKS、KeyStore等格式证书说明及转换
  11. Cesium之【空间面积】测量
  12. C2 - Skyscrapers (hard version),Codeforces Round #622 (Div. 2),单调栈
  13. 关于74HC573使用
  14. LigerUI初学篇---使用LigerUI制作简洁清爽的界面
  15. 在报表开发工具Stimulsoft Report数据透视表的新功能介绍
  16. Python入门学习之个人税务计算练习(if)
  17. 新静安二手房房价依旧上涨
  18. 宗镜录略讲——南怀瑾老师——系列4
  19. 电磁波传播相位是否会变化_浅谈电磁波相位
  20. 《惢客创业日记》2019.07.02(周二)被误解的花心大叔

热门文章

  1. XenApp/XenDesktop 7.11中对于视频、图片、文字的优化的说明
  2. Linux环境下进入MySQL环境报权限问题:Access denied for user 'root@localhost' (using password:YSE)...
  3. 最新 react 开发框架(webpack2、react15、react-router4、antd-mobile)
  4. iOS之NSString类中compare方法的陷阱
  5. beanstalkd消息队列在生产环境的应用
  6. 凌轩:中国电信在校园市场的困与囧
  7. hibernate连接池配置
  8. 验证apk签名方式(V1 || V2)
  9. 51nod 1270 数组的最大代价
  10. Java程序员面试中的多线程问题1