JC XC

一、课程描述

本课程针对从事Linux内核和应用开发以及系统性能调试调优的工程师。

本课程详细讲解Linux的工具链、进程调度、内存管理、I/O模型、系统调用等系统原理,以及多进程、多线程、I/O编程的方法,融系统负载分析、内存分析、I/O分析、内核调试、应用调试等实践于理论。在讲解具体的调试和优化方法时,紧扣底层的原理,讲解Linux的各种调试和优化工具具体数据的含义,使得工程师在调试时知其然,知其所以然,能全盘慧眼把关一个Linux系统。

This course aims at engineers working on Linux kernel and application development, and system performance profiling.

It covers awide range of Linux components, such as scheduler, memory management, I/O model, network protocol stack, multi-processes, multi-threads, system loading analysis, kernel debugging etc. While explaining the particular Linux debuggingand optimization methods, the course would dig into the principles of Linuxkernel and system, rather than give basic introductions. As a result, the course would enhance students' understanding of the Linux system architecture and the details of some important components, and boost their skills ondebugging and profiling.

二、主办单位:

中国高科技产业化研究会信号处理专家委员会 

北京中际赛威文化发展有限公司

北京中际孚歌科技有限公司

北京中际荣威科技有限公司

三、研修时间:

2017年10月25-28日(24日上课当天报到) 

四、研修地点:

上  海(具体地点及路线图详见报到通知)

五、培训对象:

嵌入式相关领域的专业人士,具有开发和设计经验的硬件或软件开发工程师、系统移植工程师。

六、课程内容:

第一天

第1章、进入 Linux的精彩世界

1.1 Linux启动过程(多核)

1.2 Linux系统组成

1.3 strace和ltrace

1.4 GNU工具链和GDB调试

1.5 GCC编译的各个阶段分解

1.6 ELF文件分析

1.7 反汇编, objdump, dwarfdump

1.8 readelf, nm,strip

1.9 GDB调试技巧: 断点、watch、内存与backtrace等

1.10 GDB与多线程

1.11 LD_PRELOAD与动态库捕获

1.12 gprof

1.13 gcov

1.14 崩溃转储core dump

第2章、Linux内核进程调度与调试

2.1 进程生命周期

2.2 调度的上下文切换以及开销

2.3 调度算法的出发点:吞吐率与响应

实验课

1.写一个工具自动分析文件系统中程序与库的依赖关系图;

2.写一个工具自动分析文件系统中程序与库的符号依赖;

3.gcov白盒覆盖率

4.gdb调试多线程

5. gdb attach到一个运行进程

6.调试coredump实例

7. 用 strace和ltrace跟踪一个应用对内核和库的调用

8. 跟踪和拦截Linux应用程序对动态库的调用

第二天

2.2 进程调度算法

2.2.1 SCHED_FIFO/RR

2.2.2 SCHED_NORMAL与CFS算法

2.2.3 nice

2.3 进程调度时机

2.4 Linux实时性与RT解决方案

2.5 SMP

2.5.1 多核负载均衡

2.5.2 CPU热插拔

2.5.3 CPU affinity

2.5.4 BFS算法

2.6 针对CPU资源的Cgroups

2.7 系列案例演示调度行为对系统响应的影响

第3章、内核调试

3.1 printk 及其变体

3.2 内核崩溃oops分析

3.3 内核debug 选项

3.4 proc 和 sys

3.5 内核启动过程调试

3.6 内核启动时间优化调试

3.7 待机和电源管理调试

3.8 用JTAG调试内核

实验课程

1.使用dev_xxx和pr_xxx打印信息

2.分析一次内核崩溃oops并反汇编

3.写一个透过/proc在用户空间和内核空间进行交互的例子

4.使用JTAG和GDB调试内核

5.运行一个多线程的程序,观察top, htop, mpstat的情况

6.通过chrt, nice, renice, taskset方法改变进程的调度属性

7.通过cgroup分配CPU资源

第三天

第4章、内存分析与内存泄露

1.  MMU系统(MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。)

2.  page与zone

3.  buddy系统

4.  slab、kmalloc

5.  用户空间malloc与内核buddy等的关系

6.  out-of-memory(OOM)与控制

7.  进程的内存消耗

8.  pagecache与swap

9.  zRAM

10.内存泄露剖析

11.Addresssanitizer与valgrind

12.针对内存资源的Cgroups

实验课程

1.分析一个运行时Linux的内存分布情况

2.用smem观察进程的内存变化

3.用valgrind跟踪一个有堆内存泄露的进程

4.启动编译器Addresssanitizer

4.写一个有栈溢出的程序并观察溢出表现

5.做I/O动作,观察page cache变化

6.运行一个引起OOM的程序

7.运行一个程序的多个副本,观察PSS变化 

第四天

第5章、Linux多进程与多线程

1.  多进程通信

2.  多线程通信

3.  正确的互斥和同步方法

4.  可重入与线程安全

5.  多进程、多线程调试

6.  IPC调试、死锁

7.  Linux的I/O模型

8.  多线程与I/O

9.  C10K问题

第6章、Linux性能优化

1. CPU负载分析:top, htop, mpstat

2. I/O负载分析:iostat, iotop

3. Linux逻辑分析仪:LTTng

4. 综合性能瓶颈:oprofile/perf

5. 程序执行时间分布分析

6. cache miss分析

7. 开机优化:bootchart

8. 功耗优化: powertop 和 cpufreq-bench

9. ftrace

10. Linux基准程序(LMBench,Bonnie++,IOZone, Netperf/iperf等)

11. 特别彩蛋: LEP(Linux Easy Profiling)

实验课程

1.观察一个有一定CPU、I/O负载的系统CPU、I/O情况

2.运行oprofile分析 binary的时间比例

3.使用Bonnie++分析文件系统的性能

4.修改diry_ratio等值分析文件系统性能

5.使用LMBench分析操作系统性能

6.观察一个LTTng的结果

7.运行perf观察CPU、I/O分布情况

8.运行perf观察程序的时间分布




++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

在监督程序时代是以作业的形式来表示程序运行的

作业以同步地方式串行地运行每个作业步

当操作系统发展到分时系统时,为了实现同一个作业中不同作业之间并发执行,作业机制已经不能满足我们的需要。因此引入了进程机制,让进程来实现作业步的执行。

在操作系统,程序以进程的方式使用系统资源,包括程序和数据所用的内存空间,系统外设,文件等

程序运行所需的系统资源,并且以分时共享的方式使用处理机资源

操作系统相关的进程管理和资源管理模块负责创建进程、为进程加载用户态运行程序、为进程分配资源、调度进程占用处理器、支持进程间通信等。

TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT


在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。如果所有的程序都能使用这些指令,那么你的系统一天死机n回就不足为奇了。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。

    linux的内核是一个有机的整体。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。

    当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。

    内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, 如上所提到的intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。Ring3状态不能访问Ring0的地址空间,包括代码和数据。Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运 行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必 须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。

     处理器总处于以下状态中的一种:

1、内核态,运行于进程上下文,内核代表进程运行于内核空间;

2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;

3、用户态,运行于用户空间。

 

从用户空间到内核空间有两种触发手段:

1.用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

2.硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

   一个程序我们可以从两种角度去分析。其一就是它的静态结构,其二就是动态过程。下图表示了用户态和内核态直接的关系(静态的角度来观察程序)

TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

线程调度间的上下文切换

什么是上下文切换?

如果主线程是唯一的线程,那么他基本上不会被调度出去。另一方面,如果可运行的线程数大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出去,从而

使其他线程能够使用CPU。这将导致一次上下文切换。在这个过程中将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。

切换上下文需要一定的开销,而在线程调度过程中需要访问操作系统和JVM共享的数据结构。应用程序、操作系统以及JVM都使用一组相同的CPU。在JVM和操作系统的代码中消耗越多的CPU时钟周期,应用程序的可用CPU时钟周期就越少。但上下文切换的开销并不只是包含JVM和操作系统的开下。当一个新的线程被切换进来时,它所需要额数据可能不在当前处理器的本地缓存中,因此上下文切换将导致一些缓存缺失,因而线程在首次调度运行时会更加缓慢。这就是为什么调度器会每个可运行的线程分配一个最小执行时间,即使有许多其他的线程正在等待执行:它将上下文切换的开销分摊到更多不会中断的执行时间上,从而提供整体的吞吐量(以损失响应性为代价)。

当线程由于等待某个发生竞争的锁而被阻塞时,JVM通常会将这个线程挂起,并允许它被交换出去。如果线程频繁的发生阻塞,那么他们将无法使用完整的调度时间片。在程序中发生越多的阻塞(包括阻塞IO,等待获取发生竞争的锁,或者在条件变量上等待),与CPU密集型的程序就会发生越多的上下文切换,从而增加调度开销,并因此降低吞吐量。(无阻塞算法同样有助于减小上下文切换)

上下文切换的实际开销会随着平台的不同而变化,然而根据经验来看:在大多数通用的处理器中,上下文切换的开销相当于5000-10000个时钟周期,也就是几微秒。

UNIX系统的vmstat命令和Windows系统的perfmon工具都能报告上下文切换的次数以及在内核中执行时间所占比例等信息。如果内核占用率较高(超过10%),那么通常表示调度活动发生的很频繁,这很可能是由于IO或竞争锁导致的阻塞引起的。。

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA


多线程编程

涉及操作系统原理概念
  时间片
  进程状态

  上下文: 对进程来说,就是进程的执行环境,具体就是各个变量和数据,包括所有的寄存器变量、打开的文件、内存信息等。

  进程的写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是,数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副本。这样做可以提高 fork的效率。

  线程函数的可重入性:所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。

  要确保函数可重入,需满足一下几个条件:

  1、不在函数内部使用静态或全局数据 
  2、不返回静态或全局数据,所有数据都由函数的调用者提供。 
  3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
  4、不调用不可重入函数。

********************************************************************

线程与进程的对比

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。

线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

进程和线程的关系:

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.

进程与线程的区别:

(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.

(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

线程相对于进程的优势

(调度、分配:更少的生成删除时间
 进程/线程之间的切换开销(挂起正在运行的进程或线程,恢复以前挂起的进程或线程):线程切换更快,不用恢复用户地址空间
  通信机制:通信更有效(共享地址空间、不需要调用内核传递信息)

  并发性
  编码之间的原理:创建过程的复杂度,及对程序的控制力度)

 (1)易于调度。

 (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

 (3)开销少。创建线程比创建进程要快,所需开销很少。。

 (4)利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

进程与线程的状态对比

进程状态

 

状态:运行、阻塞、挂起阻塞、就绪、挂起就绪

状态之间的转换:

  准备就绪的进程,被CPU调度执行,变成运行态;

     运行中的进程,进行I/O请求或者不能得到所请求的资源,变成阻塞态;

     运行中的进程,进程执行完毕(或时间片已到),变成就绪态;

     将阻塞态的进程挂起,变成挂起阻塞态,当导致进程阻塞的I/O操作在用户重启进程前完成(称之为唤醒),挂起阻塞态变成挂起就绪态,当用户在I/O操作结束之前重启进程,挂起阻塞态变成阻塞态;

     将就绪(或运行)中的进程挂起,变成挂起就绪态,当该进程恢复之后,挂起就绪态变成就绪态;

线程状态

     

同步和互斥的区别:

        当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。

      所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

        所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

********************************************************************

线程的操作(根据生命周期的图示操作)
线程的创建

 

1
2
3
#include <pthread.h>
int pthread_create(pthread_t * thread , const pthread_attr_t *attr, void *(*start_routine)( void *), void *arg);
// compile and link with -pthread  
  thread: 指向线程标识符的指针
  attr: 设置线程的属性
  start_routine: 线程运行程序的起始地址
  arg: 运行程序的参数

  void* arg : 无法通过该参数传输大量数据

 解决方法:

  为每个线程定义一个结构,结构中包括同一线程函数所需的所有数据(易于实现多线程复用同一线程函数);

等待指定线程的结束

1
2
#include <pthread.h>
int pthread_join(pthread_t thread , void **retval);

当一个进程创建的时候,一个主线程将被创建
主线程持有进程信息,主线程与新创建进程间没有显示的父子关系
函数返回时,被等线程的资源被回收(与进程不同)

一个线程不能被多个线程等待:如果这样的话第一个收到信号的线程成功返回,其他的出错ESRCH;

注意线程等待
线程的结束

1
2
#include <pthread.h>
void pthread_exit( void *retval);

 

线程的原子操作(例如银行取钱系统的操作)
进程中没有原子操作

线程原子操作:要么全部执行,要么全部不执行

  异步可删除(初始化时,线程默认是异步可删除的):线程可以在任一点被删除;

  同步可删除:设置可删除点,删除请求被放进队列,等线程完成一定的任务后再响应。

  不可删除:任何删除请求都被忽略

包含线程的程序的GCC编译
在编译参数中加入 -pthread

线程id跟进程id

进程id是可移植的,是unsigned int 类型.
线程id是一个非可移植性的类型,也就是说,在这个系统中,可能是unsigned int类型,在别的系统可能是long,double,或者甚至就是个结构体。
所以,为了准确起见,打印的时候,表示为十六进制的话,也就是:
printf("threadId is %x \n",threadId[i]); 如我们的系统获得,pthread_t是个结构体时,我们可以把这个值直接认为是十六进制的地址值.

线程应用实例
web服务器
单线程服务器(只有一个主线程)


多线程服务器(一个主线程生成多个工作线程)


一个主线程对应一个端口,故程序一般只占有一个主线程。
线程池服务器(一个主线程创建一个线程池,从线程池中取出线程用于处理用户请求)

 

进程的动态性
进程、信号的生命周期


线程的时间片
课堂练习:
新线程与主线程共享内存,主线程等待停到i=0的状态,四个新线程都创建完成了
主线程可能还停留在i=0的状态。然后新线程打印出四个0.
解决方法:设置新线程不共享内存,每个都有独立的内存空间。


ooooooooooooooooooooooooooooooooOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO



 一、fork入门知识

     一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

     我们来看一个例子:

[cpp] view plain copy

    /*
     *  fork_test.c
     *  version 1
     *  Created on: 2010-5-29
     *      Author: wangth
     */  
    #include <unistd.h>  
    #include <stdio.h>   
    int main ()   
    {   
        pid_t fpid; //fpid表示fork函数返回的值  
        int count=0;  
        fpid=fork();   
        if (fpid < 0)   
            printf("error in fork!");   
        else if (fpid == 0) {  
            printf("i am the child process, my process id is %d/n",getpid());   
            printf("我是爹的儿子/n");//对某些人来说中文看着更直白。  
            count++;  
        }  
        else {  
            printf("i am the parent process, my process id is %d/n",getpid());   
            printf("我是孩子他爹/n");  
            count++;  
        }  
        printf("统计结果是: %d/n",count);  
        return 0;  
    }  

     运行结果是:
    i am the child process, my process id is 5574
    我是爹的儿子
    统计结果是: 1
    i am the parent process, my process id is 5573
    我是孩子他爹
    统计结果是: 1
    在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
    为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

    在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

    引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
    fork出错可能有两种原因:
    1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
    2)系统内存不足,这时errno的值被设置为ENOMEM。
    创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
    fork执行完毕后,出现两个进程,

    有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。
    执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
    还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。

二、fork进阶知识

    先看一份代码:

[cpp] view plain copy

    /*
     *  fork_test.c
     *  version 2
     *  Created on: 2010-5-29
     *      Author: wangth
     */  
    #include <unistd.h>  
    #include <stdio.h>  
    int main(void)  
    {  
       int i=0;  
       printf("i son/pa ppid pid  fpid/n");  
       //ppid指当前进程的父进程pid  
       //pid指当前进程的pid,  
       //fpid指fork返回给当前进程的值  
       for(i=0;i<2;i++){  
           pid_t fpid=fork();  
           if(fpid==0)  
               printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
           else  
               printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
       }  
       return 0;  
    }  

    运行结果是:
    i son/pa ppid pid  fpid
    0 parent 2043 3224 3225
    0 child  3224 3225    0
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227
    1 child     1 3227    0
    1 child     1 3226    0
    这份代码比较有意思,我们来认真分析一下:
    第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:
    p2043->p3224->p3225
    第一次fork后,p3224(父进程)的变量为i=0,fpid=3225(fork函数在父进程中返向子进程id),代码内容为:

[c-sharp] view plain copy

    for(i=0;i<2;i++){  
        pid_t fpid=fork();//执行完毕,i=0,fpid=3225  
        if(fpid==0)  
           printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
        else  
           printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
    }  
    return 0;  

    p3225(子进程)的变量为i=0,fpid=0(fork函数在子进程中返回0),代码内容为:
[c-sharp] view plain copy

    for(i=0;i<2;i++){  
        pid_t fpid=fork();//执行完毕,i=0,fpid=0  
        if(fpid==0)  
           printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
        else  
           printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
    }  
    return 0;  

    所以打印出结果:
    0 parent 2043 3224 3225
    0 child  3224 3225    0
    第二步:假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226,对于此时的父进程,p2043->p3224(当前进程)->p3226(被创建的子进程)。
    对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227,对于此进程,p3224->p3225(当前进程)->p3227(被创建的子进程)。从输出可以看到p3225原来是p3224的子进程,现在变成p3227的父进程。父子是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。
    所以打印出结果是:
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227
    第三步:第二步创建了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。
    以下是p3226,p3227打印出的结果:
    1 child     1 3227    0
    1 child     1 3226    0
    细心的读者可能注意到p3226,p3227的父进程难道不该是p3224和p3225吗,怎么会是1呢?这里得讲到进程的创建和死亡的过程,在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的,至于为什么,这里先不介绍,留到“三、fork高阶知识”讲。
    总结一下,这个程序执行的流程如下:

     这个程序最终产生了3个子进程,执行过6次printf()函数。
    我们再来看一份代码:

[cpp] view plain copy

    /*
     *  fork_test.c
     *  version 3
     *  Created on: 2010-5-29
     *      Author: wangth
     */  
    #include <unistd.h>  
    #include <stdio.h>  
    int main(void)  
    {  
       int i=0;  
       for(i=0;i<3;i++){  
           pid_t fpid=fork();  
           if(fpid==0)  
               printf("son/n");  
           else  
               printf("father/n");  
       }  
       return 0;  
      
    }  

     它的执行结果是:
    father
    son
    father
    father
    father
    father
    son
    son
    father
    son
    son
    son
    father
    son
    这里就不做详细解释了,只做一个大概的分析。
    for        i=0         1           2
              father     father     father
                                        son
                            son       father
                                        son
               son       father     father
                                        son
                            son       father
                                        son
    其中每一行分别代表一个进程的运行打印结果。
    总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。(感谢gao_jiawei网友指出的错误,原本我的结论是“执行printf函数的次数为2*(1+2+4+……+2N)次,创建的子进程数为1+2+4+……+2N ”,这是错的)
    网上有人说N次循环产生2*(1+2+4+……+2N)个进程,这个说法是不对的,希望大家需要注意。

    数学推理见http://202.117.3.13/wordpress/?p=81(该博文的最后)。
    同时,大家如果想测一下一个程序中到底创建了几个子进程,最好的方法就是调用printf函数打印该进程的pid,也即调用printf("%d/n",getpid());或者通过printf("+/n");来判断产生了几个进程。有人想通过调用printf("+");来统计创建了几个进程,这是不妥当的。具体原因我来分析。
    老规矩,大家看一下下面的代码:

[cpp] view plain copy

    /*
     *  fork_test.c
     *  version 4
     *  Created on: 2010-5-29
     *      Author: wangth
     */  
    #include <unistd.h>  
    #include <stdio.h>  
    int main() {  
        pid_t fpid;//fpid表示fork函数返回的值  
        //printf("fork!");  
        printf("fork!/n");  
        fpid = fork();  
        if (fpid < 0)  
            printf("error in fork!");  
        else if (fpid == 0)  
            printf("I am the child process, my process id is %d/n", getpid());  
        else  
            printf("I am the parent process, my process id is %d/n", getpid());  
        return 0;  
    }  

    执行结果如下:
    fork!
    I am the parent process, my process id is 3361
    I am the child process, my process id is 3362
    如果把语句printf("fork!/n");注释掉,执行printf("fork!");
    则新的程序的执行结果是:
    fork!I am the parent process, my process id is 3298
    fork!I am the child process, my process id is 3299
    程序的唯一的区别就在于一个/n回车符号,为什么结果会相差这么大呢?
    这就跟printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了。
    运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时缓冲里面的“fork!”  被子进程复制过去了。因此在子进程度stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork!  被printf了2次!!!!
    而运行printf("fork! /n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容。因此你看到的结果会是fork! 被printf了1次!!!!
    所以说printf("+");不能正确地反应进程的数量。
    大家看了这么多可能有点疲倦吧,不过我还得贴最后一份代码来进一步分析fork函数。
[cpp] view plain copy

    #include <stdio.h>  
    #include <unistd.h>  
    int main(int argc, char* argv[])  
    {  
       fork();  
       fork() && fork() || fork();  
       fork();  
       return 0;  
    }  

    问题是不算main这个进程自身,程序到底创建了多少个进程。
    为了解答这个问题,我们先做一下弊,先用程序验证一下,到此有多少个进程。
[c-sharp] view plain copy

    #include <stdio.h>  
    int main(int argc, char* argv[])  
    {  
       fork();  
       fork() && fork() || fork();  
       fork();  
       printf("+/n");  
    }  

    答案是总共20个进程,除去main进程,还有19个进程。
    我们再来仔细分析一下,为什么是还有19个进程。
    第一个fork和最后一个fork肯定是会执行的。
    主要在中间3个fork上,可以画一个图进行描述。
    这里就需要注意&&和||运算符。
    A&&B,如果A=0,就没有必要继续执行&&B了;A非0,就需要继续执行&&B。
    A||B,如果A非0,就没有必要继续执行||B了,A=0,就需要继续执行||B。
    fork()对于父进程和子进程的返回值是不同的,按照上面的A&&B和A||B的分支进行画图,可以得出5个分支。

   

     加上前面的fork和最后的fork,总共4*5=20个进程,除去main主进程,就是19个进程了。

三、fork高阶知识

        这一块我主要就fork函数讲一下操作系统进程的创建、死亡和调度等。因为时间和精力限制,我先写到这里,下次找个时间我争取把剩下的内容补齐。

 

 

 

参考资料:

 

      http://blog.csdn.net/dog_in_yellow/archive/2008/01/13/2041079.aspx

      http://blog.chinaunix.net/u1/53053/showart_425189.html

      http://blog.csdn.net/saturnbj/archive/2009/06/19/4282639.aspx

      http://www.cppblog.com/zhangxu/archive/2007/12/02/37640.html

      http://www.qqread.com/linux/2010/03/y491043.html

      http://www.yuanma.org/data/2009/1103/article_3998.htm

awz1998

        awz1998
        2018-01-18 11:38 135楼

        讲的太棒了,感觉已经很清晰了,谢谢

bjq1016

        bjq1016
        2018-01-03 19:48 134楼

        [cpp] view plain copy
            printf("Thank you") ;  

cndm123

        cndm123
        2017-12-12 09:43 133楼

        第二段程序,我在Ubuntu下采用gcc编译,./test 执行程序。得到的结果如下:
        sa@ubuntu:~/Test$ ./test
        i = 0 , parent , 2975 , 3120 , 3121
        i = 1 , parent , 2975 , 3120 , 3122
        sa@ubuntu:~/Test$ i = 0 , child , 2190 , 3121 , 0
        i = 1 , child , 2190 , 3122 , 0
        i = 1 , parent , 2190 , 3121 , 3123
        i = 1 , child , 2190 , 3123 , 0
        总体跟你解释的一样,但是最后结果的输出顺序有点不一样

L_cheryl

        L_cheryl
        2017-12-24 09:19
        回复cndm123:因为父进程和子进程哪个先执行是不确定的,和环境又关系,只要顺序满足拓扑序就是正确的

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值