1.进程概述
进程和程序的区别
程序:程序是存储在存储介质上的一个可执行文件---静态的
进程:进程是程序的执行实例。可以说进程就是正在执行的程序。
程序是一些指令的集合,而进程是程序的执行过程,这个过程的状态是变化的,包括进程的创建、调度和消亡。
单道程序和多道程序
单道程序:所有进程一个一个排队执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态。而在人机交互时阻塞的出现是必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。以前的dos操作系统就是单道程序系统
多道程序:在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行(并行和并发)。多道程序设计必须有硬件基础作为保证。
并行和并发
并行:同一时间内,多个程序或多条指令同时运行(一定是多核)
并发:宏观上的并行,多个进程使用同一块资源(CPU),但是他们快速的交叉使用CPU资源,给人一种假象,像是多个程序同时执行。
进程控制块(PCB-process contrl block)
进程在内存中运行时,内核为每个进程分配一个 PCB(进程控制块),维护进程相关的信息(包括数据、代码存放点位置灯),Linux 内核的进程控制块是 task_struct 结构体。相当于人的身份证。
PCB是操作系统中最重要的记录型数据结构。
2.进程的状态
三种状态
就绪态:进程已经具备执行的一切条件,正在等待CPU的处理时间
执行态:该进程正在占用CPU运行
等待态:进程因不具备某些执行条件而暂时无法继续执行的状态
ps指令:用于显示当前进程的状态,类似于windows的任务管理器
参数:
ps 的参数非常多, 在此仅列出几个常用的参数并大略介绍含义
-A 列出所有的进程
-w 显示加宽可以显示较多的资讯
-au 显示较详细的资讯
-aux 显示所有包含其他使用者的进程
-ajx 更详细、更具层次关系
USER:进程拥有者
PID:process id
%CPU:占用CPU使用率
%NAME:占用的记忆体使用率
VSZ:占用的虚拟记忆体大小
RSS:占用的记忆体大小
TTY:终端的次要装置号码
STAT:该进程的状态
START:进程开始时间
TIME:执行的时间
COMMAND:执行的命令
stat状态:
3.进程号
每个进程都有一个进程号来进标识,其类型是pid_t,进程号的范围是0~32767(不同的版本可能有区别)。进程号总是唯一的,但是可以重用,一个进程终止后,这个进程号可以再次被使用。
- linux中的进程号从0开始
- 进程号0和1由内核创建
- 0进程:调度进程,常被称为交换进程
- 1进程:init进程
- 除调度进程外,所有进程都由1进程进行直接或者间接调用
- 进程号0和1由内核创建
PID:进程号,是一个非负整数
PPID:父进程,任何进程都由另一个进程创建,高金城称为被创建进程的父进程。对应的进程号称为父进程号(PPID);
PGID:进程组,一个或多个进程的集合。他们之间相互关联,进程组可以接受同一终端的各种信号,关联的进程有一个进程组号。
获取进程函数
#include <sys/types.h>
#include <unistd.h>
//功能:获取本进程号
//返回值:当前进程进程号
pid_t getpid(void);
//功能:获取调度此函数的进程的父进程号
pid_t getppid(void);
//功能:获取进程组好,参数为0是返回当前PGID,否则返回指定的进程的PGID
pid_t getpgid(pid_t pid);
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("该进程号:%d\n",getpid());
printf("该进程的父进程号:%d\n",getppid());
printf("该进程的进程组号:%d\n",getpgid(0));
return 0;
}
4.创建进程
fork函数(创建的子进程是复制一份父进程的内容,到一个新的空间)
#include <sys/types.h>
#include <unistd.h>
//创建一个新进程
pid_t fork(void)
功能:
/*fork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进
程称为父进程。
返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。 非常重要!!!!!
失败:返回-1*/
注意:子进程是将父进程里面的内容全部拷贝了一份,但是子进程里的fork不会再执行,会从fork的下一句开始执行,即将得到的id赋值给pid,所以fork函数在父进程里返回值为子进程id,在子进程里返回值为0(系统自动处理,避免一直执行fork,子子孙孙无穷尽也)
因为父子进程同时再运行,所以才会有两个结果(否则 if与elseif是互斥的,不可能有两个结果)
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
pid_t pd = fork();
if (pd < 0)
{
perror("fork");
return 1;
}
else if (pd == 0)
{
while (1)
{
printf("子进程ID-->%d,父进程ID-->%d\n", getpid(), getppid());
sleep(5);
}
}
else if (pd > 0)
{
while (1)
{
printf("父进程ID-->%d\n", getpid());
sleep(2);
}
}
return 0;
}
sleep函数
是将进程暂时挂起一段时间,在后续资源回收详细介绍,可以在此处理解为延时一段时间
#include<unistd.h>
unsigned int sleep(unsigned int sec); //进程挂起一段时间,即一直处在等待态
功能:
进程挂起指定的描述,知道指定的时间用完或者收到信号才解除挂起
返回值:
若进程挂起到sec指定的时间,则返回0,若有信号中断,则返回剩余的秒数
注意:
进程挂起指定的秒数后程序不会立即执行,系统知识将此进程切换到就绪态
父子进程的复制
使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。
地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只有它的进程号,计时器等。因此,使用 fork 函数的代价是很大的。
进程之间都是相互独立的。
vfork函数(子进程和父进程共享一块内存空间)
#include <sys/types.h>
#include <unistd.h>
//pid_t vfork(void);创建一个新进程,但是vfork在创建进程的时候,先创建子进程,在创建父进程
/*功能:vfork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程,子进程需要退出之后(exit()),父进程才可以执行。
返回值:成功:子进程中返回 0,父进程中返回子进程 ID。失败:返回-1。*/
pid_t vfork(void);
先创建子进程,再创建父进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pd = vfork();
if (pd<0)
{
perror("vfork");
}
else if (pd > 0)
{
while (1)
{
sleep(1);
printf("当前进程id:%d\n",getpid());
_exit(1); //退出,结束当前进程
}
}
else if (pd == 0)
{
while (1)
{
sleep(1);
printf("当前进程id:%d 当前进程父进程:%d\n",getpid(),getppid());
_exit(1); //退出,结束当前进程
}
}
return 0;
}
子进程父进程共享同样的内存
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int num = 0;
pid_t pd = vfork();
if (pd < 0)
{
perror("vfork");
}
else if (pd == 0)
{
while (1)
{
sleep(1);
printf("当前进程id:%d 当前进程父进程:%d\n", getpid(), getppid());
num = 1000;
printf("num:%d\n", num);
_exit(1); // 退出,结束当前进程
}
}
else if (pd > 0)
{
while (1)
{
sleep(1);
printf("当前进程id:%d\n", getpid());
printf("num:%d\n", num);
_exit(1); // 退出,结束当前进程
}
}
return 0;
}
5.进程资源回收(等待)
当进程结束后,系统可以回收进程资源,但是关于进程中的例如ID号,内存地址,进程名等,系统不会回收。父子进程有序需要简单的进程间同步,比如父进程等待子进程结束。
一般指的是父进程回收子进程。
wait函数:
#include <sys/types.h> #include <sys/wait.h> /* 功能:等待子进程终止,如果子进程终止了,那么此函数会回收子进程的资源。调用wait函数的进程会挂起(阻塞),因为要等待子进程结束。如果有多个子进程,需要调用多个wait函数 参数:status,子进程退出时的状态,需要提前定义,后续会把状态存储到这里 返回值: 成功:子进程的进程号 失败:-1 */ pid_t wait(int *wstatus);
案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd < 0)
{
perror("fork");
}
else if (pd == 0)
{
int i = 0;
for ( i = 5; i > 0; i--)
{
printf("子进程%u剩余生命:%d\n",getpid(),i);
sleep(1);
}
exit(-1);
}
else if (pd > 0)
{
int status = 0 ;
printf("父进程%u正在等待子进程%u结束\n",getpid(),pd);
wait(&status);
printf("子进程%u结束.\n",pd);
printf("退出状态为%d\n",status);
}
return 0;
}
exit函数
#include <stdlib.h>
void exit(int status); //退出进程,这是库函数写法,等价于_exit(int status)
wait参数status详解
1.取出子进程的退出信息 WIFEXITED(status) 如果子进程是正常终止的,取出的字
段值非零。2. WEXITSTATUS(status) 返回子进程的退出状态,退出状态保存在status 变量的 8~16 位。在用此宏前应先用宏 WIFEXITED 判断子进程是否正常退出,正常退出才可以使用此宏。
3.此 status 是个 wait 的参数指向的整型变量
else if (pd > 0)
{
int status = 0;
printf("父进程%u正在等待子进程%u结束\n", getpid(), pd);
pid_t son = wait(&status); //保存退出的那个pid
if (WIFEXITED(status)) //如果正常退出
{
printf("子进程%u结束.\n", son);
printf("退出状态为%d\n", WEXITSTATUS(status)); //打印退出状态
}
}
waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
函数文档注释:
功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid : 参数 pid 的值有以下几种类型:
pid > 0 等待进程 ID 等于 pid 的子进程。
pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程
组,waitpid 不会等待它。
pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。例如传入-5555,对应的作用是监控进程组号为5555的每一个pid
status : 进程退出时的状态信息。和 wait() 用法一样。
options : options 提供了一些额外的选项来控制 waitpid()。
0:同 wait(),阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回(非阻塞)。
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程
的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)返回值: waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 的进程发现有子进程在运行,而且没有已退出的子进程,则返回0;如果父进程所有子进程都结束,则返回-1;如果>0,则等到了一个子进程退出,这个返回值就是退出的那个子进程
可等待
3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误
所在。
代码案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd < 0)
{
perror("fork");
}
else if (pd == 0)
{
int i = 0;
for (i = 5; i > 0; i--)
{
printf("子进程%u剩余生命:%d\n", getpid(), i);
sleep(1);
}
exit(0);
}
else if (pd > 0)
{
int status = 0;
printf("父进程%u正在等待子进程%u结束\n", getpid(), pd);
sleep(6);
pid_t son = waitpid(-1,&status,WNOHANG); //6s后检测是否有子进程退出,如果有,则返回对应进程id
printf("son %u\n",son);
if (WIFEXITED(status))
{
printf("子进程%u结束.\n", son);
printf("退出状态为%d\n", WEXITSTATUS(status));
}
}
return 0;
}
6.特殊进程
6.1僵尸进程
进程已经结束,但是对应的资源没有回收,这样的进程称之为僵尸进程。例如:子进程已退出,但是父进程未回收子进程资源(wait,waitpid),那么此子进程就成为僵尸进程。
有危害:因为子进程id被占用,但是系统的pid数量有限制
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd < 0)
{
perror("fork");
}
else if (pd == 0)
{
int i = 0;
for (i = 5; i > 0; i--)
{
printf("子进程%u剩余生命:%d\n", getpid(), i);
sleep(1);
}
exit(0);
}
else if (pd > 0)
{
//子进程结束,父进程不回收资源
while (1);//父进程不能结束,否则他们会一起被回收
}
return 0;
}
6.2孤儿进程
父进程先结束,但是子进程未运行结束的子进程。
子进程被1号进程接管,当子进程结束时,由1号进程进行资源回收
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd < 0)
{
perror("fork");
}
else if (pd == 0)
{
while (1);
int i = 0;
for (i = 5; i > 0; i--)
{
printf("子进程%u剩余生命:%d\n", getpid(), i);
sleep(1);
}
exit(0);
}
else if (pd > 0)
{
printf("父进程%u结束\n",getpid());
}
return 0;
}
6.3守护进程(精灵进程)
脱离终端的孤儿进程
暂时不写,后续再写。继续往后学下去吧
7.fork创建多个子进程
创建2个子进程
1.错误的写法
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
for (int i = 0; i < 2; i++)
{
pid_t pid = fork();
}
while(1);
return 0 ;
}
有4个a.out相关进程
实际是1个父进程,2个子进程,还有1个子进程的子进程,一共4个。
这样会有问题,不知道哪个进程是子进程
2.正确的进程
起始上面的代码只要能保证子进程不会再创建子进程即可
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
for (int i = 0; i < 2; i++)
{
pid_t pid = fork();
if (pid == 0) //如果是在子进程里,就不再往下运行
{
break;
}
}
while(1);
return 0 ;
}
3.创建多进程并且回收资源
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int i = 0;
for (i = 0; i < 2; i++)
{
pid_t pid = fork();
if (pid == 0)
{
break;
}
}
if (i == 0)
{
printf("第1个子进程id:%u\n",getpid());
sleep(3);
exit(1);
}
else if(i == 1)
{
printf("第2个子进程id:%u\n",getpid());
sleep(4);
exit(1);
}
else if(i == 2)
{
int ret = 0;
printf("父进程Id:%u\n",getpid());
while(1)
{
ret = waitpid(-1,NULL,WNOHANG);
if (ret == 0)
{
continue;
}
else if (ret > 0)
{
printf("进程%u退出\n",ret);
}
else if(ret < 0)
{
break;
}
}
}
return 0 ;
}
8.终端
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为Shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB中的信息,而 fork 会复制 PCB 中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。
举个例子:
int num = 0;
scanf("%d",&num);
printf("%d\n",num);
while(1);
在终端中,得到一个shell进程,这个shell进程的PCB中保存着终端控制信息,因此他可以控制终端。
当检测到a.out的时候,会fork一个子进程来完成a.out的执行,fork的子进程里面不会再有./a.out这个执行执行
子进程中没有./a.out,也会执行,这是由exec族函数完成的,后续会讲。此时shell会把中断控制权交给a.out,所以在a.out执行的时候,输入任何命令都不起作用。a.out结束后,终端控制权移交给shell进程
此时,如果a.out也创建一个子进程呢?
代码案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd == 0)
{
int num = 0;
sleep(1);
scanf("%d",&num);
printf("子进程%u中num=%d,所属终端名为%s\n",getpid(),num,ttyname(0));
}
else if (pd > 0)
{
int num = 0;
sleep(3);
scanf("%d",&num);
printf("父进程%u中num=%d,所属终端名为%s\n",getpid(),num,ttyname(0));
}
return 0 ;
}
ttyname函数
/*获取终端名称
返回值:
成功:终端名
失败:NULL
参数:文件描述符*/
char *ttyname(int fd);
进程组
进程组是包含一个或多个进程的集合,属于一个回话,fork不会改变进程组。
进程组ID
每个进程组都有唯一的进程组ID(整数),进程组由进程组ID来唯一标识,除了PID,进程组ID也是一个进程的必备属性之一。
进程组ID一般是当前进程组中的第一个进程的ID(组长)
getpgid();//获得进程组ID
int setpgid(pid_t pid,pid_t pgid);//将pid的进程组id设置为pgid。创建一个新进程组或者假如一个已经存在的pgid
会话
多个进程组的集合叫做会话。
没打开一个终端,必打开一个会话。
如果进程ID=进程组ID=会话ID,那么该进程为会话首进程(会长)
会话是一个或多个进程组的集合。 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备; 建立与控制终端连接的会话首进程被称为控制进程; 一个会话中的几个进程组可分为一个前台进程组以及一个或多个后台进程组; 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组; 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
前台进程组:该进程组中的进程能够向终端设备进行读、写操作的进程组。
后台进程组:只能向终端写的进程组。
创建会话
创建会话的函数:
#include <sys/types.h>
#include <unistd.h>
/*
函数功能:创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。
返回值:
成功:返回调用进程的会话 ID
失败:-1
*/
pid_t setsid(void);
注意事项
1) 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2) 该调用进程是组长进程,则出错返回
3) 该进程成为一个新进程组的组长进程
4) 需有 root 权限(ubuntu 不需要)
5) 新会话丢弃原有的控制终端,该会话没有控制终端
6) 建立新会话时,先调用 fork, 父进程终止,子进程调用 setsid
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
pid_t pd = fork();
if (pd == 0)
{
printf("%u\n",getsid(0));
setsid();
while(1)
{
}
}
else if (pd > 0)
{
exit(1);
}
return 0 ;
}
9.exec函数族
在 Windows 平台下,我们可以通过双击运行可执行程序,让这个可执行程序成为一个进程;而在 Linux 平台,我们可以通过 ./ 运行,让一个可执行程序成为一个进程。 但是,如果我们本来就运行着一个程序(进程),我们如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里我们通过 exec 函数族实现。(即可以实现从一个运行的终端启动另一个程序)。
exec 指的是一组函数,一共有 6个
核心特点:
核心特点总结(四大特点)
“换魂不换壳”
不换:进程ID(PID)、父进程、打开的文件描述符、信号设置、环境变量等全部保留原样。
全换:进程的代码段、数据段、堆栈等被彻底替换为指定的新程序。
一山不容二虎
exec
调用成功后没有返回值,因为原来的程序代码已经被完全覆盖了,执行逻辑永远跳不到exec
之后的代码。如果调用失败(如找不到指定程序),则会返回
-1
,并设置errno
,然后继续执行原程序的后续代码。一族六将,功能各异
这是一个函数族,包含多个函数(如execl
,execv
,execle
,execve
,execlp
,execvp
),它们最终都调用同一个系统调用execve。
#include <unistd.h>
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file,cconst char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char
* const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
一个进程调用 exec 后,除了进程 ID,进程还保留了下列特征不变: 父进程号 进程组号 控制终端 根目录 当前工作目录 进程信号屏蔽集 未处理信号 ...
六个exec函数只有execve是真正的系统调用函数,其他的都是在此基础上封装的库函数。
代码案例1:execl执行ls指令
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
execl("/bin/ls","-a","-l",NULL);
printf("hello world\n");
return 0 ;
}
exclp执行ls命令(p:即path,env下面的path都包含)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//execl("/bin/ls","-a","-l",NULL);
execlp("ls","-a","-l",NULL);
printf("hello world\n");
return 0 ;
}
execv实现:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//execl("/bin/ls","-a","-l",NULL);
char *temp[]={"-a","-l",NULL};
execv("/bin/ls",temp);
printf("hello world\n");
return 0 ;
}
execve实现
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//execl("/bin/ls","-a","-l",NULL);
extern char **environ;
char *temp[]={"-a","-l",NULL};
execve("/bin/ls",temp,environ);
printf("hello world\n");
return 0 ;
}
system函数
#include <stdlib.h>
/*system函数会调用fork函数,产生子进程,子进程会调用exec启动/bin/sh -c string来执行参数string所代表的命令,命令执行完毕后返回原调用进程
参数:要执行的字符串
返回值:
如果command为null,则返回非0,一般为1
如果system()在调用/bin/sh时候失败,返回127,其他原因返回-1
注意:system调用成功才会返回对应的返回值,如果连fork都调用失败,肯定不会成功,一般返回0为成功
*/
int system(const char *command);
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int status = system("ls -al");
if (WIFEXITED(status))
{
printf("the exit status is %d\n",status);
}
else
{
printf("非正常退出\n");
}
return 0 ;
}