【Linux 篇】数字世界的通信管道 —— 匿名管道与进程池深度实战解析
于此止个人主页❄️个人专栏传送门《Linux篇》 《QT百日筑基篇》 《算法篇》《C与STL剖析篇》 《数据结构篇》 《Python基础篇》⭐️纵有狂风拔地起我亦乘风破万里⭐️匿名管道与进程池目录/索引目录匿名管道与进程池目录/索引一、进程间通信的背景二、进程间通信介绍三、怎么进行通信四、进程间通信的发展五、进程间通信的分类1.管道2.system V IPC3.POSIX IPC六、管道什么是管道命令行中管道的使用七、匿名管道1.通信方式2.通信的原理3.创建匿名管道4.利用匿名管道进行进程间的通信任务1.管道的创建。任务2.创建子进程。任务3对文件的读写操作。源码八、匿名管道的5中特性九、管道的四种通信情况十、进程池的编写验证我们管道的容量文件类型进程池的结构进程池代码剖析结构框架成员变量进程池的数量创建进程池子进程的工作运行进程池信道的选择进程池的销毁Bug进程池的源码ProccessPool.hpptasktable.hpp:test.cc一、进程间通信的背景Linux 中每个进程拥有独立虚拟地址空间进程内存相互隔离无法直接访问对方数据。实际开发经常需要多进程协同工作、分发任务、交换数据因此诞生了进程间通信IPC。匿名管道是经典轻量 IPC 方式适用于存在亲缘关系的父子进程非常适合用来实现进程池任务分发。Linux 进程具备内存隔离特性进程之间不能直接共享数据。想要实现多进程协同、构建进程池调度任务就需要进程间通信。匿名管道作为简洁高效的通信方案常被用于父子进程之间的数据传输。什么是通信呢通信就是将信息从发送方传递到接收方的过程。 分为模拟通信、数字通信也可分为有线通信、无线通信。 一套通信系统包含信源、发送设备、信道、接收设备、信宿传输过程会受到噪声干扰。日常手机、网络、蓝牙、光纤等都属于通信应用二、进程间通信介绍1.数据传输一个进程需要将消息发送给另一个进程。2.资源共享多个进程共享同一份资源。3.通知事件一个进程需要向另一个或者另一组进程发消息通知他们发生了某些事并执行某种任务子进程退出时会将自己的退出信息告诉父进程。4.进程控制有些进程希望自己去控制另一个进程Debug进程此时控制进程能够拦截另一个进程的所有陷入和异常 并能够通知他们他的状态发生改变。三、怎么进行通信我们先来说一说进程间通信的本质让不同的进程看到同一份的资源。内存这分资源由我们os进行开辟空间。os会给我们进行提供对应的系统调用的接口。我们去进行调用这块接口去完成对应的操作。os在设计这块的接口的时候会统一设置这块的接口来进行控制这份资源。四、进程间通信的发展1.管道。2.system V进程间的通信。3.POSIX进程间的通信。五、进程间通信的分类1.管道匿名管道pipe命名管道2.system V IPC-system V消息队列-system V共享内存-system V信号量3.POSIX IPC-消息队列-共享内存-信号量-互斥锁-条件变量-读写锁六、管道什么是管道管道是Unix最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流叫做管道。管道是内核级的。命令行中管道的使用我们下面的|就代表管道这个命令的意思是我们首先使用ls -l打印出当前目录的所有的文件在使用我们的grep过滤出我们想要的文件。这样我们对应的命令行就能给我们打印出来对应的文件信息下面就是我们管道的一个简单的使用以后我们会用于ps等命令中让我们更快更好的过滤出我们想要的信息。ls -l | grep code.c七、匿名管道匿名管道是我们当时基于现有的技术文件操作直接进行通信。1.通信方式1.基于文件的管道通信。2.基于system的本机通信。2.通信的原理匿名管道通常是我们父子间的通信。我们对应创建子进程时我们的子进程会拷贝父进程的数据进行操作。拷贝到的内容是和父进程是一样的只有我们进行修改的时候才会发生对应的写实考呗。那么这样我们在创建子进程的时候成功让我们的不同的进程看到同一份资源。那么我们就可以进行对应的进程间的通信了。下图有个小问题我们在创建对应的子进程的时候我们的文件还要在重新的进行深拷贝吗答案是不用的我们对应的struct_file这个表储存的是文件的描述符我们只要通过对应的文件描述符就能找到对应的文件 我们后面他会不会打开文件和我们的进程没有任何关系。因为我们前面属于进程管理我们后面的一部分属于文件管理。所以我们创建子进程时会将文件描述符表给我们进行拷贝一份这次的拷贝属于是浅拷贝。我们可以先创建一个管道 然后我们的创建一个子进程这是我们的父子进程的读写端都链接到这个管道这里 这时候我们将父进程的读端关闭 我们将子进程的写端进行关闭这样我们就能让父进程进行向管道中写内容我们的子进程向管道中读内容这样我们就能完成对应的进程间的通信了并且看到了同一份资源管道这份资源是我的子进程继承了父进程的资源。同理我们的父进程管写子进程关读也是可以的。3.创建匿名管道我们这边使用pipe进行匿名管道的创建我们这边使用pipe进行匿名管道的创建我们首先看一下我们的手册man 2 pipe我们这需要传过去一个int类型的数组 他会将我们对应创建好的管道的2文键描述符该我们进行返回2个一个读端一个写端。我们对应创建出来的管道没有名字因此叫做匿名管道。返回值成功返回0失败返回-1错误码被设置。4.利用匿名管道进行进程间的通信任务1.管道的创建。ok这里我们在看一下文档。头文件unistd.h,要先创建一个fd有两个空间的数组。然后我们调用pipe。我们可以将我们对应管道的文件描述符给打印出来对应是几号文件描述符。int main() { int status[2] {0};//创建对应的数组 int ret_pipe pipe(status);//创建管道 if (ret_pipe 0) {//判断是否成功 perror(pipe false); exit(1); } std::cout status[0]:status[1]std::endl;//判断对应的数组的内容 return 0; }注意这里申请的是3号和4号 这两个号其中3号是读端我们可以这样进行记忆 status对应的status[0]这个0像一个嘴巴在读所以他就是我们对应的读端。也就是我们对应的3号文件描述符。我们对应的status[1]像一支笔我们要写一些东西也就代表我们的4号文件描述符。他的文件描述符也是支持就近原则的谁小有限先择谁。任务2.创建子进程。下一步我们就要创建对应的子进程来完成对应的任务了我们这里是让我们的子进程进行写入让我们的父进程进行读取。所以我们子进程要进行写入所以我的子进程要关闭对应的读端。我们将我们子进程要干的活设置为对应的child函数进行封装。我们的父进程要进行对应的读取我们的父进程关闭对应的写端我们让父进程的工作用函数parent来进行封装。所以我们子进程要进行对应的写入父进程要进行我们对应的读取这不就是我们的文件操作了吗。int main() { int status[2] {0}; int ret_pipe pipe(status); if (ret_pipe 0) { perror(pipe false); exit(1); } // std::cout status[0]:status[1]std::endl; // 创建对应的进程 pid_t id fork(); if (id 0) { close(status[0]); child(status[1]); close(status[1]); } close(status[1]); parent(status[0]); waitpid(id, 0, 0); close(status[0]); return 0; }任务3对文件的读写操作。我们子进程向文件写就行了我们的父进程一直的读取就行了。所以现在我们就是基于文件进行操作void child(int wfd) { char buffer[1024]; int cnt 0; while (1) { snprintf(buffer, sizeof(buffer), I am child pid:%d,cnt: %d, getpid(), cnt); write(wfd, buffer, strlen(buffer)); sleep(1); } } void parent(int wfd) { char buffer[1024]; while(1) { buffer[0]0; ssize_t nread(wfd, buffer,sizeof(buffer)-1); if(n0) { buffer[n]0; std::coutchild say: bufferstd::endl; } sleep(1); } }ok我们的一个匿名管道就做好了我们进行运行一下通信我们也发现他是1s中进行打印一条消息。源码#include cstdio #include cstring #include iostream #include sys/types.h #include sys/wait.h #include unistd.h void child(int wfd) { char buffer[1024]; int cnt 0; while (1) { snprintf(buffer, sizeof(buffer), I am child pid:%d,cnt: %d, getpid(), cnt); write(wfd, buffer, strlen(buffer)); sleep(1); } } void parent(int wfd) { char buffer[1024]; while(1) { buffer[0]0; ssize_t nread(wfd, buffer,sizeof(buffer)-1); if(n0) { buffer[n]0; std::coutchild say: bufferstd::endl; } sleep(1); } } int main() { int status[2] {0}; int ret_pipe pipe(status); if (ret_pipe 0) { perror(pipe false); exit(1); } // std::cout status[0]:status[1]std::endl; // 创建对应的进程 pid_t id fork(); if (id 0) { close(status[0]); child(status[1]); close(status[1]); exit(0); } close(status[1]); parent(status[0]); waitpid(id, 0, 0); close(status[0]); return 0; }八、匿名管道的5中特性我们通过上面的试验也能看出来管道的这5中特性。1.匿名管道只能进行有血缘关系的进程才能通信。兄弟之间也是可以的2.管道文件带有同步机制。你写一条我读一条3.管道是面向字节流的。4.匿名管道是单项通信的。一端写一端读-一个写一个读 ——半双工-两个都能写都能读 —— 全双工5.管道的生命周期是随进程的。底层有对应的引用计数九、管道的四种通信情况1.写慢读快 —— 读端阻塞等写端写写一条读一条。我们只要把对应的写段sleep上几秒就可以。其余代码保持不变。基于上面的源码void child(int wfd) { char buffer[1024]; int cnt 0; while (1) { snprintf(buffer, sizeof(buffer), I am child pid:%d,cnt: %d, getpid(), cnt); write(wfd, buffer, strlen(buffer)); sleep(3); } }2.写快读慢 —— 写端一直写读端一读读出一大串的内容。写完了写端阻塞等读。我们只要把对应的读端sleep上几秒就可以。其余代码保持不变。基于上面的源码void parent(int wfd) { char buffer[1024]; while(1) { buffer[0]0; ssize_t nread(wfd, buffer,sizeof(buffer)-1); if(n0) { buffer[n]0; std::coutchild say: bufferstd::endl; sleep(2); } sleep(1); } }3.写关读不关 —— 写端会默认读到最后一个字符返回0读端也要进行关闭。我们让程序写一次就进行关闭break外面会调用close进行关闭我们的写端关闭我们的读端默认就是读到了文件的结尾read返回值为0其余代码保持不变。基于上面的源码void child(int wfd) { char buffer[1024]; int cnt 0; while (1) { snprintf(buffer, sizeof(buffer), I am child pid:%d,cnt: %d, getpid(), cnt); write(wfd, buffer, strlen(buffer)); break; } } void parent(int wfd) { char buffer[1024]; while(1) { buffer[0]0; ssize_t nread(wfd, buffer,sizeof(buffer)-1); if(n0) { buffer[n]0; std::coutchild say: bufferstd::endl; } else if(n 0) { std::coutw退出了我也退出std::endl; break; } } }4.写不关读关 —— 程序异常 父进程一直等待回收子进程子进程一直在哪里写写满等读。读一次我们就关闭我们看到我们读了一次我们的程序就会阻塞在哪里 我们的子进程没有退出父进程就会阻塞在哪里想要回收子进程。其余代码保持不变。基于上面的源码十、进程池的编写我们现在编写的进程池是基于我们的匿名管道来实现我们对应的多进程之间的操作的。验证我们管道的容量我们将读端进行关闭运行下面的一个程序我们每一次写4个字节看看一共能写多少。void child(int wfd) { char buffer[1024]; int cnt 0; while (1) { write(wfd, cnt, 4); cnt; std::coutcntstd::endl; } }我们看到了一共16385次那我们计算一下16384 x 4 65540 Byte 65540 / 1024 约等于64个字节。所以我们能进行使用的是64个字节左右。文件类型我们先认识我们对应的文件类型我们会用到对应的hpp文件 他是我们.h文件和.cpp文件的合并形式。我们之前定义.h文件会进行分文件去实现对应的.cpp文件 而我们.hpp文件定义完可以直接的进行使用。我们的源文件.cc .cpp .cxx都是我们对应的c文件的后缀 我们哪个文件都可以进行使用。进程池的结构我们可以创建一个父进程来进行写 然后pipe创建很多个管道来进行对应的通信这里设置为5个然后创建5个子进程fork来进行读取 每当我父进程写入的时候我们对应的这5个子进程来进行对应的读取 我们能设置对应的读写规则。父进程写入通过管道写入到不同的子进程中。进程池代码剖析1.我们要有的结构类首先我们每一个信道要进行创建Channel 就代表一个信道。其次我们的信道要进行对应的组织起来。方便我们后续的管理。Grp_Channel 然后就是编写我们的进程池的一个专属的类负责提供对应的接口。Propool 结构框架我们的结构如下#include iostream class Channel { Channel() { } ~Channel() { } }; class Grp_Channel { Grp_Channel() { } ~Grp_Channel() { } }; class Propool { Propool() { } ~Propool() { } };成员变量那我们对应的成员都有啥呢一定有我们的文件描述符fd我们创建管道一定需要读写所以一定要添加fd。我们的Grp_Channel就是将我们的Channel进行组织起来。所以我们可以选择一个数据结构这里选择vectorlist map都可以。我们的进成池一定要有一个Grp_Channel类型的变量。当然还有对应的成员我们后面进行加入。所以class Channel { public: Channel() { } ~Channel() { } private: int fd; int id; std::string name; }; class Grp_Channel { public: Grp_Channel() { } ~Grp_Channel() { } private: std::vectorChannel _cn; }; class Propool { public: Propool() { } ~Propool() { } private: Grp_Channel _cm; };进程池的数量我们这里要设置管道的数量。我们默认是5。也就代表了我们子进程的数量。#define PROC_MAX 5创建进程池在使用进程池通信之前我们要先创建对应的进程池。我们的子进程要执行读的工作所以我们的Work所以我们专们写一个函数来进行执行。我们的父进程要做的工作就是把我们的文件描述符给保存起来有助于我们进行管理。bool Create() { for (int i 0; i _num; i) { int pipefd[2] {0}; int ret_pipe pipe(pipefd); if (ret_pipe 0) { perror(pipe fail); return false; } pid_t id fork(); if (id 0) { close(pipefd[1]); Work(pipefd[0]); close(pipefd[1]); exit(0); } else if (id 0) { close(pipefd[0]); _cm.Push(id, pipefd[1]); } else { perror(fork fail!); return false; } } return true; }void Push(int id, int fd) { _vc.emplace_back(id, fd); }Channel(pid_t id, int fd) : _id(id), _fd(fd) { name 我是一个子进程- std::to_string(fd) -std::to_string(id); }子进程的工作这里我们的子进程要干什么呢当然是读取我们对应的数据数了。这里我们设置一个整数code表示一个任务码我们会发出很多的任务码便于我们执行不同的任务。所以我们设置一个code。这里注意如果我们写端不写他就会阻塞在那里。所以刚开始的时候就会进行阻塞。我们的父进程负责发对应的任务码。void Work(int fd) { while (1) { int code0; ssize_t ret_r read(fd, code, sizeof(code)); if (ret_r 0) { perror(read fail!); } else if (ret_r 0) { std::cout w端退出 std::endl; break; } else { if(ret_rsizeof(code)) { std::cout子进程[ getpid]收到了一个退出码std::endl; } else continue; } } }运行进程池这里要注意我们发出了对应的任务码必须先创建对应的任务所以这里我们要先写我们有什么的任务。这里随便给出了三个任务。我们的Register是我们想要向文件中添加什么任务。Code是我们返回一个随机任务让我们进行执行。Execute这个主要是给我们的子进程进行设置的让我们的子进程的得到对应的任务码的时候去执行对应的函数。#pragma once #include iostream #include vector #include ctime typedef void (*task_t)(); ////////////////debug///////////////////// void PrintLog() { std::cout 我是一个打印日志的任务 std::endl; } void Download() { std::cout 我是一个下载的任务 std::endl; } void Upload() { std::cout 我是一个上传的任务 std::endl; } ////////////////////////////////////// class TaskManager { public: TaskManager() { srand(time(nullptr)); } void Register(task_t t) { _tasks.push_back(t); } int Code() { return rand() % _tasks.size(); } void Execute(int code) { if(code 0 code _tasks.size()) { _tasks[code](); } } ~TaskManager() {} private: std::vectortask_t _tasks; };所以我们在运行进程池的时候要注意这些任务我们要先进性注册对应任务。ProcessPool(int num):_num(num) { _tb.Register(PrintLog); _tb.Register(Download); _tb.Register(Upload); } TaskManager _tb;这里我们的父进程要先去选择一个任务让我们去执行。然后选择一个信道让我们去执行。我们向这个信道去发送这个任务码。void Proc_Start() { //选择一个任务 int taskcode _tb.Code(); //选择一个信道来完成任务。 autoc_cm.Select(); std::cout选择了一个子进程c.GetName()std::endl; //发送对应的任务来完成 c.Send(taskcode); std::cout发送了一个对应的任务码taskcodestd::endl; }信道的选择我们采用什么方式进行选择我们的选则是避免忙的忙死闲的闲死。1.轮询的方式进行2.随机选择3.添加负载指标所以这里我们原则轮询的方式Channel Select()//轮询的方式进行查找 { Channel ret _vc[_next]; _next; _next % _vc.size(); return ret; }我们要加入一个_next的成员变量让我们找下一个进行工作的信道。_next % _vc.size();为了避免我们进行越界。我们这里在提供一种随机srand(0); int a rand() % _vc.size(); return _vc[a];我们选择了这个信道让这个信道去write到管道中让对应的进程来读取对应的任务码来进行去解决对应的任务。void Send(int code) { ssize_t n write(_fd, code, sizeof(code)); (void)n; }然后我们基于现在的子进程还要再加入对应的执行函数让我们执行对应任务码对应的任务。void Work(int fd) { while (1) { int code0; ssize_t ret_r read(fd, code, sizeof(code)); if (ret_r 0) { perror(read fail!); } else if (ret_r 0) { std::cout w端退出 std::endl; break; } else { if(ret_rsizeof(code)) { std::cout子进程[ getpid]收到了一个退出码std::endl; //执行对应的这个任务 _tb.Execute(code); } else continue; } } }上面我已经写好了一个对应的信道的运行和创建进程池的销毁我们的进程池的销毁主要是由2的东西1.我们的父进程回收所有的子进程 2.我们的文件描述符要进行关闭。这里我们的管道要不要进行销毁呢答案不要因为我们对应的管道是对应的引用计数当我们的引用技术为0的时候我们的OS回自行进行回收。我们的进程池只需要调用一次void StoProcPool()//终止进程池 { _cm.delete_Stop(); }Grp_Channelvoid start_close() { for(auto vc : _vc) { vc.Close(); } } void start_wait() { for(auto vc: _vc) { vc.Wait(); } }Channel我们的id和fd在我们的一个个信道中就有了。所以我们循环进行调用就可以了。void Close() { close(_fd); } void Wait() { pid_t wi waitpid(_id,nullptr,0); (void) wi; }Bug当然这个代码有一个很深的BUG我们可以知道当我们创建一个子进程的时候我们回拷贝他的文件描述符表所以我们下一个创建的子进程文件描述符表中会有指向我们上一个创建管道。以此类推我们的下下一个会指向我们的前两个的管道。我们可以从下开始进行对应的释放我们对应的表//解决方案1从下向上进行回收 // std::vectorChannel::reverse_iterator it _vc.rbegin(); // while(it ! _vc.rend()) // { // it-Close(); // std::cout关闭it-GetName()std::endl; // it-Wait(); // std::cout回收it-GetName()std::endl; // it; // }方案二提前进行一次将文件描述符我们对应不需要的进行关闭。在创建的时候我们就把不要的文件描述符进行关闭就可了我们对应的通道就一定只有我们的2个进程进行指向了。_cm.start_close();//进程是独立的我们在父进程获取的一些元素文件描述符我们进行关闭//我们的文件描述符会不会关闭呢答案是不会的因为就算我们的父进程先发生push也会发生写实考呗。for(auto vc : _vc) { vc.Close(); std::cout关闭vc.GetName()std::endl; vc.Wait(); std::cout回收vc.GetName()std::endl; }进程池的源码ProccessPool.hpp#pragma once #include cstring #include iostream #include sys/types.h #include unistd.h #include vector #include sys/wait.h #include tasktable.hpp #define CHANNEL_NUM 5 #define READ_MAX 1024 // 先描述 class Channel { public: Channel(pid_t id, int fd) : _id(id), _fd(fd) { name 我是一个子进程- std::to_string(fd) -std::to_string(id); } ~Channel() { } std::string GetName() { return name; } pid_t Getpid() { return _id; } int Getfd() { return _fd; } void Send(int code) { ssize_t n write(_fd, code, sizeof(code)); (void)n; } void Close() { close(_fd); } void Wait() { pid_t wi waitpid(_id,nullptr,0); (void) wi; } private: pid_t _id; int _fd; std::string name; }; // 在组织 class AdmChannel { public: AdmChannel() :_next(0){ } void Push(int id, int fd) { _vc.emplace_back(id, fd); } Channel Select()//轮询的方式进行查找 { Channel ret _vc[_next]; _next; _next % _vc.size(); return ret; // srand(0); // int a rand() % _vc.size(); // return _vc[a]; } void PrintCe()//测试 { for(auto vc:_vc) { std::coutvc.GetName()std::endl; } } void delete_Stop() { //解决方案二 从源头上解决我们的原因是不同的文件描述符指向了同一块区域。我们提供一个close提前进行关闭。 for(auto vc : _vc) { vc.Close(); std::cout关闭vc.GetName()std::endl; vc.Wait(); std::cout回收vc.GetName()std::endl; } //解决方案1从下向上进行回收 // std::vectorChannel::reverse_iterator it _vc.rbegin(); // while(it ! _vc.rend()) // { // it-Close(); // std::cout关闭it-GetName()std::endl; // it-Wait(); // std::cout回收it-GetName()std::endl; // it; // } } void start_close() { for(auto vc : _vc) { vc.Close(); } } void start_wait() { for(auto vc: _vc) { vc.Wait(); } } ~AdmChannel() {} private: std::vectorChannel _vc; int _next; }; class ProcessPool { public: ProcessPool(int num):_num(num) { _tb.Register(PrintLog); _tb.Register(Download); _tb.Register(Upload); } void Work(int fd) { while (1) { int code0; ssize_t ret_r read(fd, code, sizeof(code)); if (ret_r 0) { perror(read fail!); } else if (ret_r 0) { std::cout w端退出 std::endl; break; } else { if(ret_rsizeof(code)) { std::cout子进程[ getpid]收到了一个退出码std::endl; //执行对应的这个任务 _tb.Execute(code); } else continue; } } } bool Create() { for (int i 0; i _num; i) { int pipefd[2] {0}; int ret_pipe pipe(pipefd); if (ret_pipe 0) { perror(pipe fail); return false; } pid_t id fork(); if (id 0) { close(pipefd[1]); _cm.start_close();//进程是独立的我们在父进程获取的一些元素文件描述符我们进行关闭 //我们的文件描述符会不会关闭呢答案是不会的因为就算我们的父进程先发生push也会发生写实考呗。 Work(pipefd[0]); close(pipefd[1]); exit(0); } else if (id 0) { close(pipefd[0]); _cm.Push(id, pipefd[1]); } else { perror(fork fail!); return false; } } return true; } void Proc_Start() { //选择一个任务 int taskcode _tb.Code(); //选择一个信道来完成任务。 autoc_cm.Select(); std::cout选择了一个子进程c.GetName()std::endl; //发送对应的任务来完成 c.Send(taskcode); std::cout发送了一个对应的任务码taskcodestd::endl; } void Debug() { _cm.PrintCe(); } void StoProcPool()//终止进程池 { _cm.delete_Stop(); } ~ProcessPool() {} private: AdmChannel _cm; int _num; TaskManager _tb; };tasktable.hpp:#pragma once #include iostream #include vector #include ctime typedef void (*task_t)(); ////////////////debug///////////////////// void PrintLog() { std::cout 我是一个打印日志的任务 std::endl; } void Download() { std::cout 我是一个下载的任务 std::endl; } void Upload() { std::cout 我是一个上传的任务 std::endl; } ////////////////////////////////////// class TaskManager { public: TaskManager() { srand(time(nullptr)); } void Register(task_t t) { _tasks.push_back(t); } int Code() { return rand() % _tasks.size(); } void Execute(int code) { if(code 0 code _tasks.size()) { _tasks[code](); } } ~TaskManager() {} private: std::vectortask_t _tasks; };test.cc#include ProcessPool.hpp int main() { std::cout你好进程池std::endl; //创建进程池 ProcessPool pp(CHANNEL_NUM); pp.Create(); //启动进程 int cnt 10; while(cnt--) { pp.Proc_Start(); sleep(1); } pp.StoProcPool(); return 0; }