进程的定义
进程的概念首先是在60年代初期由MIT的Multics系统和IBM的TSS/360系统引入的。在40多年的发展中,人们对进程有过各种各样的定义。现列举较为著名的几种:
进程是一个独立的可调度的活动(E. Cohen,D. Jofferson)
进程是一个抽象实体,当它执行某个任务时,要分配和释放各种资源(P. Denning)
进程是可以并行执行的计算单位。(S. E. Madnick,J. T. Donovan)
以上进程的概念都不相同,但其本质是一样的。它指出了进程是一个程序的一次执行的过程,同时也是资源分配的最小单元。
它和程序是有本质区别的,程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;
而进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。它是程序执行和资源管理的最小单位。
进程控制块和标识符
进程是Linux系统的基本调度和管理资源的单位,它是通过进程控制块(PCB)来描述的。
进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述,是操作系统核心中一种数据结构。
在Linux中,进程控制块是一个task_struct结构体。
在Linux中最主要的进程标识有进程号(PID,Process Idenity Number)和它的父进程号(PPID,parent process ID)。其中PID惟一地标识一个进程。PID和PPID都是非零的正整数。
在Linux中获得当前进程的PID和PPID的系统调用函数为getpid()和getppid()。
另外,进程标识还有用户和用户组标识、进程时间、资源利用情况等
获取进程号例子:
#include
int main()
{
printf("This current process pid is %d\n",getpid());
printf("This current process parent pid is %d\n",getppid());
return 0;
}
进程的状态
进程是程序的执行过程,根据它的生命周期可以划分成3种状态。
执行态:该进程正在运行,即进程正在占用CPU。
就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间片。
等待态:进程不能使用CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
Linux下进程地址空间
Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。
Linux中的进程包含3个段,分别为“数据段”、“代码段”和“堆栈段”。
“数据段”存放的是全局变量、常数以及动态数据分配的数据空间,根据存放的数据,数据段又可以分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
“代码段”存放的是程序代码的数据。
“堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。
Linux下的进程管理
进程相关命令
创建进程
在Linux中创建一个新进程的方法是使用fork()函数。
fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。
使用fork()函数得到的子进程是父进程的一个复制品。而子进程所独有的只有它的进程号、资源使用和计时器等。
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序,相同的PC值。因此需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程不可能做不同的事。
在父进程中执行fork()函数时,父进程会复制出一个子进程,
而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行。
从而两个进程分别获得其所属fork()的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0。
因此,可以通过返回值来判定该进程是父进程还是子进程。
同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork()函数的系统开销比较大,而且执行速度也不是很快。
fork代码实战:
#include
#include
#include
#include
int main()
{
int result;
result = fork();//fork后,父子进程同时从此处开始执行
printf("The result is %d\n",result);
if(-1==result)
{
printf("fork error\n");
return -1;
}
else if(0==result)//判断当前是子进程
{
printf("In child process!!\nmy pid is %d\nmy father pid is %d\n",getpid(),getppid());
}
else//判断当前是父进程
{
printf("In parent process!!\nmy pid is %d\nmy child pid is %d\n",getpid(),result);
wait();//等待子进程先退出,不然子进程父进程就会变成1号进程
}
return 0;
}
exec函数族
exec函数族就提供了一个在进程中启动另一个程序执行的方法。
它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。
另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
使用exec函数族主要有两种情况
当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用exec函数族中的任意一个函数让自己重生;
如果一个进程想执行另一个程序,那么它就可以调用fork()函数新建一个进程,然后调用exec函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
参数传递的形式
1)execv开头的函数是以"char *argv[]"这样的形式传递命令行参数。
2)execl开头的函数采用了我们更习惯的列表方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
环境变量的传递
1)execle和execve使用了char *envp[]传递环境变量。
2)其它的4个函数则将把默认的环境变量不做任何修改地传给被执行的应用程序。
所调用程序名的传递
1)execlp和execvp的第1个参数file可以简单到仅仅是一个文件名,如"ls",这两个函数会自动到环境变量PATH指定的目录里去寻找。
linux中PATH查看方法:echo $PATH
2)除execlp和execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如"/bin/ls";
exec函数族代码实战:
execlp.c
/*execlp.c*/
#include
#include
#include
int main()
{
int ret;
if ((ret = fork()) == 0)
{
/*调用execlp函数,这里相当于调用了"ps -ef"命令*/
if ((ret = execlp("ps", "ps", "-ef", NULL)) < 0)
{
printf("Execlp error\n");
}
}
return ret;
}
execl.c
/* execl.c */
#include
#include
#include
int main()
{
int ret;
if ((ret = fork()) == 0)
{
/*调用execl函数,注意这里要给出ps程序所在的完整路径*/
if ((ret = execl("/bin/ps","ps","-ef",NULL)) < 0)
{
printf("Execl error\n");
}
}
return ret;
}
execle.c
/* execle.c */
#include
#include
#include
int main()
{
/*命令参数列表,必须以NULL结尾*/
char *envp[]={"PATH=/tmp","USER=david", NULL};
if (fork() == 0)
{
/*调用execle函数,注意这里也要指出env的完整路径*/
if (execle("/usr/bin/env", "env", NULL, envp) < 0)
{
printf("Execle error\n");
}
}
}
execve.c
/* evecve.c */
#include
#include
#include
int main()
{
/*命令参数列表,必须以NULL结尾*/
char *arg[] = {"env", NULL};
char *envp[] = {"PATH=/tmp", "USER=david", NULL};
if (fork() == 0)
{
if (execve("/usr/bin/env", arg, envp) < 0)
{
printf("Execve error\n");
}
}
}
exit()和_exit()
在Linux的标准函数库中,有一种被称作“缓冲I/O(buffered I/O)”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,认为已经被写入到文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用exit()函数.
exit()和_exit()函数都是用来终止进程的。
_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;
exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。
代码实战:
exit.c
/* exit.c */
#include
#include
/*调用exit之后程序退出,printf语句正常输出*/
int main()
{
printf("Using exit...\n");
printf("This is the content in buffer");
exit(0);
}
__exit.c
/* _exit.c */
#include
#include
/*调用__exit之后程序退出,printf语句无法输出*/
int main()
{
printf("Using _exit...\n");
printf("This is the content in buffer");
_exit(0);
}
wait()和waitpid()
wait()函数是用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则wait()就会立即返回。
waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作业控制。
实际上wait()函数只是waitpid()函数的一个特例
wait()函数直接调用的就是waitpid()函数。
代码实战:
waitpid.c
/* waitpid.c */
#include
#include
#include
#include
#include
int main()
{
pid_t pc, pr;
pc = fork();
if (pc < 0)
{
printf("Error fork\n");
}
else if (pc == 0) /*子进程*/
{
/*子进程暂停5s*/
sleep(5);
/*子进程正常退出*/
exit(0);
}
else /*父进程*/
{
/*循环测试子进程是否退出*/
do
{
/*调用waitpid,且父进程不阻塞*/
pr = waitpid(pc, NULL, WNOHANG);
/*若子进程还未退出,则父进程暂停1s*/
if (pr == 0)
{
printf("The child process has not exited\n");
sleep(1);
}
} while (pr == 0);
/*若发现子进程退出,打印出相应情况*/
if (pr == pc)
{
printf("Get child exit code: %d\n",pr);
}
else
{
printf("Some error occured.\n");
}
}
return 0;
}
程序执行流程: