程序和进程
- 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)
- 进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)
- 站在程序员的角度来说:进程是执行一系列指令的过程。
- 站在操作系统的角度来说:进程是系统资源分配的基本单位。
- 程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具...)
- 同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)
- 如:同时开两个终端。各自都有一个bash但彼此ID不同。
进程状态
- 进程状态:一个进程的生命周期可以划分为一组状态,这些状态刻画了整个进程。进程状态即体现一个进程的生命状态。
- 进程状态:一般来说,进程有三个状态,即就绪状态,运行状态,阻塞状态。
- 就绪态:进程已经具备运行条件,但是CPU还没有分配过来;
- 运行态:进程占用CPU,并在CPU上运行;
- 阻塞态:进程因等待某件事发生而暂时不能运行;
下面是3种状态转换图
当然理论上上述三种状态之间转换分为六种情况:
- 运行——>就绪:1.主要是进程占用CPU的时间过长,而系统分配给该进程占用CPU的时间是有限的;2.在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出CPU,该进程便由执行状态转变为就绪状态。
- 就绪——>运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU。
- 运行——>阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如发生了I/O请求。
- 阻塞——>就绪:进程所等待的事件已经发生,就进入就绪队列。
以下两种状态是不可能发生的:
- 阻塞——>运行:即使给阻塞进程分配CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取。
- 就绪——>阻塞:就绪态根本就没有执行,谈不上进入阻塞态。
并发
- 并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但是在任一个时刻点上仍只有一个进程在运行。例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。
![](https://i-blog.csdnimg.cn/blog_migrate/b49fdc8d1195509c857aea7f9cf1234b.png)
单道程序设计
- 所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
多道程序设计
- 在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
- 时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
- 在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。1s = 1000ms, 1ms = 1000us, 1us = 1000ns 1000000000
- 实质上,并发是宏观并行,微观串行! -----推动了计算机蓬勃发展,将人类引入了多媒体时代。
CPU和MMU
![](https://i-blog.csdnimg.cn/blog_migrate/510112875680d9a20a2e1333d140a55b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a1f382e68e43b1a3d281c3daeeba7291.png)
MMU作用
- MMU处理地址映射功能之外,还能给不同的地址空间设置不同的访问属性。比如操作系统把自己的内核程序地址空间设置为用户模式下不可访问,这样的话用户应用程序就无法访问到该空间,从而保证操作系统内核的安全性。MPU与MMU的区别在于它只有给地址空间设置访问属性的功能而没有地址映射功能。
- 用户空间映射到物理内存是独立的(安全性),内核空间映射到同一块。
进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。其内部成员有很多,我们重点掌握以下部分即可:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有就绪、运行、挂起、停止等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 当前工作目录(Current Working Dir ectory)。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 会话(Session)和进程组。
- 进程可以使用的资源上限(Resource Limit)。ulimit -a查看所有资源上限。
环境变量
- 环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:① 字符串(本质) ② 有统一的格式:名=值[:值] ③ 值用来描述进程环境信息。
- 存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
- 使用形式:与命令行参数类似。
- 加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
- 引入环境变量表:须声明环境变量。extern char ** environ;
常见环境变量
- 按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
- PATH:可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:$ echo $PATH
- SHELL:当前Shell,它的值通常是/bin/bash。
- TERM:当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
- LANG:语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
- HOME:当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
进程控制
fork函数 创建一个子进程。
- pid_t fork(void); 失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0
- pid_t 类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)
- 注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。
getpid函数 获取当前进程ID
- pid_t getpid(void);
getppid函数 获取当前进程的父进程ID
- pid_t getppid(void);
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int var = 34;
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1)
{
perror("fork");
exit(-1);
}
else if (pid > 0)
{
sleep(2);
var = 55;
printf("I'm parent pid = %d, parentID = %d, var = %d\n", getpid(), getppid(), var);
}
else if (pid == 0)
{
var = 100;
printf("I'm child pid = %d, parentID = %d, var = %d\n", getpid(), getppid(), var);
}
printf("var = %d\n", var);
return 0;
}
运行结果
getuid函数 获取当前进程实际用户ID
- uid_t getuid(void);
geteuid函数 获取当前进程有效用户ID
- uid_t geteuid(void);
getgid函数 获取当前进程使用用户组ID
- uid_t getuid(void);
getegid 获取当前进程有效用户组ID
- gid_t getegid(void);
查看进程信息
- ps -aux
- ps -ajx 可以追溯进程间的血缘关系
循环创建n个子进程
- 一次fork函数调用可以创建一个子进程。那么创建N个子进程应该怎样实现呢?简单想,for(i = 0; i < n; i++) { fork() } 即可。但这样创建的是N个子进程吗?
循环创建N个子进程
- 从上图我们可以很清晰的看到,当n为3时候,循环创建了(2^n)-1个子进程,而不是N的子进程。需要在循环的过程,保证子进程不再执行fork ,因此当(fork() == 0)时,子进程应该立即break;才正确。
练习:通过命令行参数指定创建进程的个数,每个进程休眠1S打印自己是第几个被创建的进程。如:第1个子进程休眠0秒打印:“我是第1个子进程”;第2个进程休眠1秒打印:“我是第2个子进程”;第3个进程休眠2秒打印:“我是第3个子进程”。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int n, i; //默认创建5个子进程
if (argc == 2)
{
n = atoi(argv[1]);
}
for (i = 0; i < n; i++) //出口1,父进程专用出口
if (fork() == 0)
break; //出口2,子进程出口,i不自增
if (n == i)
{
sleep(n);
printf("I am parent, pid = %d, parent pid = %d\n", getpid(), getppid());
}
else
{
sleep(i);
printf("I'm %dth child, pid = %d, parent pid = %d\n", i + 1, getpid(), getppid());
}
return 0;
}
运行结果:
进程共享
- 父子进程之间在fork后。有哪些相同,那些相异之处呢?
- 刚fork之后:
- 父子相同处:全局变量、.text、.data、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
- 父子不同处: ①进程ID;②fork返回值;③父进程ID;④进程运行时间;⑤闹钟(定时器) ;⑥未决信号集
- 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
- 当然不是!!!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
注意
- 父进程与子进程每个打开的文件描述符共享一个文件表项。
- 在fork之后处理文件描述符有2种常见情况:(1)父进程等待进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行读、写操作的任何共享文件描述符的偏移量已经做了相应更新;(2)父进程和子进程各执行不同的程序段。在fork之后,父进程和子进程各自关闭他们不需要使用的文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main(void)
{
int fd;
pid_t pid;
fd = open("./text", O_RDWR | O_CREAT | O_TRUNC, 0755);
if (fd < 0)
{
perror("open error");
exit(-1);
}
pid = fork();
if (pid < 0)
{
perror("fork error");
exit(-1);
}
else if (pid == 0) // son
{
write(fd, "hello ", strlen("hello "));
}
else // parent
{
sleep(1);
write(fd, "world!\n", strlen("world!\n"));
wait(NULL);
}
return 0;
}
练习:编写程序测试,父子进程是否共享全局变。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 100; //.data
int main(void)
{
pid_t pid;
pid = fork();
if(pid == 0)
{ //son
a = 2000;
printf("child, a = %d\n", a);
}
else
{
sleep(1); //保证son先运行
a = 3000;
printf("parent, a = %d\n", a);
}
printf("a = %d\n", a);
return 0;
}
运行结果
全局变量也是遵循读时共享写时复制原则。
exec函数族
- fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
- 将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
- 留心看一下这6个函数还可以发现,前3个函数都是以execl开头的,后3个都是以execv开头的,它们的区别在于,execv开头的函数是以"char *argv[]"这样的形式传递命令行参数,而execl开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
- 在全部6个函数中,只有execle和execve使用了char *envp[]传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve会用指定的环境变量去替代默认的那些。
- 其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
- 区分一个函数是“系统函数”还是“库函数”依据:①是否访问内核数据结构;②是否访问外部硬件资源;二者有任一系统函数;二者均无 库函数
- exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
- 与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
- 现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和拥护做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
- 事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
execl函数 加载一个进程, 通过 路径 + 程序名来加载。
int execl(const char *path, const char *arg, ...); 成功:无返回;失败:-1
- 对比execlp,如加载"ls"命令带有-l,-F参数
- execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在PATH中搜索。
- execl("/bin/ls", "ls", "-l", "-F", NULL); 使用参数1给出的绝对路径搜索。
execlp函数 加载一个进程,借助PATH环境变量
int execlp(const char *file, const char *arg, ...); 成功:无返回;失败:-1
- 参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
- 该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1 )
{
perror("fork");
exit(-1);
}
else if (pid > 0)
{
sleep(3);
printf("I'm parent pid = %d, parentID = %d\n", getpid(), getppid());
}
else if (pid == 0)
{
//sleep(3);
printf("I'm child pid = %d, parentID = %d\n", getpid(), getppid());
//execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
perror("exec");
exit(1);
}
printf("-------finish...%d\n", getpid());
return 0;
}
运行结果
execvp函数 加载一个进程,使用自定义环境变量env
int execvp(const char *file, const char *argv[]);
- 变参形式: ①... ② argv[] (main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, ...))
- 变参终止条件:① NULL结尾 ② 固参指定
- execvp与execlp参数形式不同,原理一致。
exec函数族一般规律
- exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
- l (list) 命令行参数列表
- p (path) 搜素file时使用path变量
- v (vector) 使用命令行参数数组
- e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
- 事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
孤儿进程
- 孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程
- 僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
- 特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?(杀死它父亲)
回收子进程
wait函数
- 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
- 父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态(退出原因)。
pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 ;2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)。可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status) 为非0 → 进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
*3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(-1);
}
else if(pid == 0)
{ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(10);
exit(10);
}
else
{
printf("I'm process parent, pid = %d\n", getpid());
wpid = wait(&status); //传出参数
if(WIFEXITED(status))
{ //正常退出
printf("I'm parent, The child process %d exit normally\n", wpid);
printf("return value: %d\n", WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{ //异常退出
printf("The child process exit abnormally, killed by signal %d\n", WTERMSIG(status));//获取信号编号
}
else
{
printf("other...\n");
}
}
return 0;
}
运行结果
waitpid函数 作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程ID;失败:-1(无子进程)
特殊参数和返回情况:
参数pid:
- > 0 回收指定ID的子进程
- -1 回收任意子进程(相当于wait)
- 0 回收和当前调用waitpid一个组的所有子进程
- < -1 回收指定进程组内的任意子进程
options:
- 0与wait相同,也会阻塞;
- WNOHANG如果当前没有子进程退出,会立刻返回
返回值:
- 如果设置了WNOHANG,那么如果没有子进程退出,返回0;如果子进程退出,返回退出进程ID。
- 失败,返回-1。(没有子进程)
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
int flg = 0;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(-1);
}
else if(pid == 0) // son
{
printf("I'm process child, pid = %d\n", getpid());
sleep(5);
exit(10);
}
else // parent
{
int stat_val;
do {
//wpid = waitpid(-1, &stat_val, WNOHANG); // 回收任意子进程(相当于wait)
wpid = waitpid(pid, &stat_val, WNOHANG); // 回收指定ID的子进程
//wpid = wait(NULL);
printf("---wpid = %d--------%d\n", wpid, flg++);
if(wpid == 0)
{
printf("NO child exited\n");
sleep(1);
}
} while (wpid == 0); //子进程不可回收
if(wpid == pid)
{ //回收了指定子进程
printf("I'm parent, I catched child process, pid = %d\n", wpid);
// 进程退出状态
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
else
{
printf("other...\n");
}
}
return 0;
}
运行结果
waitpid回收多个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int n, i;
pid_t p, q;
if(argc == 2)
{
n = atoi(argv[1]);
}
for(i = 0; i < n; i++) // 创建个进程
{
p = fork();
if(p == 0)
{
break;
}
}
if(n == i) // parent
{
int stat_val;
sleep(n);
printf("I am parent, pid = %d\n", getpid());
for (i = 0; i < n; i++)
{
q = waitpid(0, &stat_val, WNOHANG);
printf("wait pid = %d\n", q);
// 进程退出状态
if (WIFEXITED(stat_val))
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else if (WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
}
else
{
sleep(i);
printf("I'm %dth child, pid = %d\n", i+1, getpid());
exit(i);
}
return 0;
}
运行结果