03Linux进程控制系统编程.docx
《03Linux进程控制系统编程.docx》由会员分享,可在线阅读,更多相关《03Linux进程控制系统编程.docx(20页珍藏版)》请在冰豆网上搜索。
03Linux进程控制系统编程
LINUX进程控制
1.Linux进程概述
进程是一个程序一次执行的过程,是操作系统动态执行的基本单元。
进程的概念主要有两点:
第一,进程是一个实体。
每个进程都有自己的虚拟地址空间,包括文本区、数据区、和堆栈区。
文本区域存储处理器执行的代码;数据区存储变量和动态分配的内存;堆栈区存储着活动进程调用的指令和本地变量。
第二,进程是一个“执行中的程序”,它和程序有本质区别。
程序是静态的,它是一些保存在磁盘上的指令的有序集合;而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建、调度和消亡的过程,是Linux的基本调度单位。
只有当处理器赋予程序生命时,它才能成为一个活动的实体,称之为进程。
内核的调度器负责在所有的进程间分配CPU执行时间,称为时间片(timeslice),它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。
1.1.进程标识
OS会为每个进程分配一个唯一的整型ID,做为进程的标识号(pid)。
进程0是调度进程,常被成为交换进程,它不执行任何程序,是内核的一部分,因此也被成为系统进程。
进程除了自身的ID外,还有父进程ID(ppid)。
也就是说每个进程都必须有它的父进程,操作系统不会无缘无故产生一个新进程。
所有进程的祖先进程是同一个进程,它叫做init进程,ID为1,init进程是内核自举后的第一个启动的进程。
init进程负责引导系统、启动守护(后台)进程并且运行必要的程序。
它不是系统进程,但它以系统的超级用户特权运行。
1.2.进程的状态
进程是程序的执行过程,根据它的生命周期可以划分成3种状态。
●执行态:
该进程正在运行,即进程正在占用CPU。
●就绪态:
进程已经具备执行的一切条件,正在等待分配CPU的处理时间片。
●等待态:
进程不能使用CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
1.3.Linux下的进程结构及管理
Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。
也就是说,进程之间是分离的任务,拥有各自的权利和责任。
其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
Linux中的进程包含以下几个部分:
·“堆栈段”存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。
“数据段”放全局变量、常数以及动态数据分配的数据空间。
数据段分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
·“正文段”存放的是CPU执行的机器指令部分。
·
进程process:
是os的最小单元地址空间大小为4g,其中1g给os3g给进程{代码区数据区堆栈}
ps:
查看活动进程。
ps–aux:
查看各个进程状态,包括运行就绪等待等状态。
ps-aux|grep'aa':
查找指定(aa)进程。
ps–ef:
查看所有的进程的pid,ppid等信息。
ps-aux看%cpu(cpu使用量)%mem(内存使用量)stat状态{S就绪T中断R运行Z僵尸}
via.c&(&表示后台运行),jobs查看后台任务,fg1把后台任务带到前台ctrl+z把进程带入后台
kill-9进程号杀掉某个进程top显示前20条进程,动态的改变pgrep'vi'查找进程
nice改变优先级crontab计划任务,定时操作等
2.Linux进程控制
2.1.fork函数
原型:
#include
pid_tfork(void);
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。
新进程为子进程,而原进程为父进程。
它和其他函数的区别在于:
它执行一次返回两个值。
其中父进程的返回值是子进程的进程号,而子进程的返回值为0.若出错则返回-1.因此可以通过返回值来判断是父进程还是子进程。
fork函数创建子进程的过程为:
使用fork函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。
通过这种复制方式创建出子进程后,原有进程和子进程都从fork函数返回,各自继续往下运行,但是原进程的fork返回值与子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在子进程中,fork返回0,如果fork返回负值,表示创建子进程失败。
(vfork函数)其作用和返回值与fork相同,但有一些区别。
二者都创建一个子进程,但是它并不是将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),所以也就不会存放该地址空间。
而且vfok保证子进程比父进程先运行,在它调用exec或exit之后父进程才可能被调度运行。
示例:
#include
#include
intmain()
{
fork();
//fork();
//fork();
printf("HelloWorld\n");
return0;
}
自己运行上面的程序看看结果是什么,并思考为什么会这样。
如果有多个fork()呢?
并输出其返回值。
示例:
#include
#include
intmain()
{
intm=100;
printf("aaaa\n");
intn=fork();
if(n>0)
{
m++;
printf("bbbbn=%dm=%d\n",n,m);
}
else
{
sleep
(2);
printf("bbbbn=%dm=%d\n",n,m);
}
return0;
}
示例:
#include
#include
#include
intmain()
{
printf("父进程的id:
%d\n",getpid());
pid_tn=fork();
if(n==0)
{
printf("子进程的id:
%dppid:
%d\n",getpid(),getppid());
}
else
{
sleep(3);
printf("创建子进程成功,子进程id:
%d\n",n);
}
return0;
}
此时相当于有两份main函数代码的拷贝,其中一份做的操作是if(iRet==0)的情况;另外一份做的操作是else(父)的情况。
所以可以输出2句话。
用fork继承打开的文件
fork以后的子进程自动继承了父进程的打开的文件,继承以后,父进程关闭打开的文件不会对子进程造成影响。
示例:
#include
#include
#include
#include
#include
intmain()
{
charbuf[32]={'\0'};
intfd=open("./a.txt",O_RDONLY);
if(fork()>0)//父进程
{
close(fd);
return0;
}
/*子进程*/
sleep(3);//waitforparentprocessclosingfd
read(fd,buf,sizeof(buf)-1);
printf("string:
%s\n",buf);
close(fd);
return0;
}
2.2.进程的终止
进程的终止有5种方式:
●main函数的自然返回;
●调用exit函数
●调用_exit函数
●接收到某个信号。
如ctrl+cSIGINTctrl+\SIGQUIT
●调用abort函数,它产生SIGABRT信号,所以是上一种方式的特例。
前3种方式为正常的终止,后2种为非正常终止。
但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。
只是后两种终止会导致程序有些代码不会正常的执行,比如对象的析构、atexit函数的执行等。
exit和_exit函数都是用来终止进程的。
当程序执行到exit和_exit时,进程会无条件的停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。
但是它们是有区别的,exit和_exit的区别如图所示:
exit函数和_exit函数的最大区别在于exit函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲”。
由于linux的标准函数库中,有一种被称作“缓冲I/O”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。
每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。
比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit函数直接将进程关闭,缓冲区中的数据就会丢失。
因此,如想保证数据的完整性,建议使用exit函数。
exit和_exit函数的原型:
#include//exit的头文件
#include//_exit的头文件
voidexit(intstatus);
void_exit(intstatus);
status是一个整型的参数,可以利用这个参数传递进程结束时的状态。
一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。
但我们是可以任意指定它的值的。
Example1:
exit的举例如下:
#include
#include
intmain()
{
printf("hello\n");
printf("world");
exit(0);
}
可以发现,调用exit函数,缓冲区中的记录也能正常输出。
Example2:
_exit的举例如下:
#include
#include
intmain()
{
printf("hello\n");
printf("world");
_exit(0);
}
可以发现,最后的输出结果没有world,说明_exit函数无法输出缓冲区中的记录。
2.3.wait和waitpid函数
用fork函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程(即init)接管。
孤儿进程退出后,它的清理工作由祖先进程init自动处理。
但在init进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免。
Example1:
写一个孤儿进程:
#include
#include
#include
main()
{
pid_tpid=fork();
if(pid==0)
{
printf("子进程…\n");
while
(1);
}
else
{
printf("父进程8秒后退出…\n");
sleep(8);
printf("父进程退出\n");
exit(10);
}
}
通过ps–ef就可以看到此时子进程一直在运行,并且父进程是1号进程。
如果子进程先退出,系统不会自动清理掉子进程的环境和资源,而必须由父进程调用wait或waitpid函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
函数原型:
#include
#include
pid_twait(int*status);
pid_twaitpid(pid_tpid,int*status,intoptions);
wait和waitpid都将暂停父进程,等待一个子进程退出,并进行清理工作;
wait函数随机地等待一个子进程退出,并返回该子进程的pid;
waitpid等待指定pid的子进程退出;如果为-1表示等待任意子进程,同样返回该子进程的pid。
status参数是传出参数,它会将子进程的退出码保存到status指向的变量里。
比如waitpid(pid,&n,0);其中n是之前定义的整型变量。
通常用下面的两个宏来获取状态信息:
WIFEXITED(n)如果子进程正常结束,它就取一个非0值。
WEXITSTATUS(n)如果WIFEXITED非零,它返回子进程的退出码,即子进程的exit()函数的参数的值,或者return返回的值。
options用于改变waitpid的行为,其中最常用的是WNOHANG,它表示无论子进程是否退出都将立即返回,不会将调用者的执行挂起。
Example1:
写一个僵尸进程:
#include
#include
#include
main()
{
pid_tpid=fork();
if(pid==0)
{
exit(10);
}
else
{
sleep(10);
//while
(1);
}
}
通过用ps–aux快速查看发现Z的僵尸进程。
Example2:
避免僵尸进程:
(wait()函数)
#include
#include
#include
#include
#include
intmain(void)
{
pid_tret,rew;
ret=fork();
if(ret==0)
{
printf("子进程的pid=%d\n",getpid());
sleep
(2);//sleep(260);
exit(10);
}
else
{
/*等待子进程结束*/
intn;
rew=waitpid(ret,&n,0);
printf("父进程中rew=%dret=%d\n",rew,ret);
printf("n=%d\n",n);
sleep(10);
}
return0;
}
通过运行测试发现,如果子进程中是exit(m)或者returnm,则父进程n的值为(m%256)*256。
2.4.exec函数族
exec*由一组函数组成
externchar**environ;
intexecl(constchar*path,constchar*arg,...);
intexeclp(constchar*file,constchar*arg,...);
intexecle(constchar*path,constchar*arg,...,char*constenvp[]);
intexecv(constchar*path,char*constargv[]);
intexecvp(constchar*file,char*constargv[]);
intexecve(constchar*path,char*constargv[],char*constenvp[]);
exec函数族的作用是运行第一个参数指定的可执行程序。
但其工作过程与fork完全不同,fork是在复制一份原进程,而exec函数执行第一个参数指定的可执行程序之后,这个新程序运行起来后也是一个进程,而这个进程会覆盖原有进程空间,即原有进程的所有内容都被新运行起来的进程全部覆盖了,所以exec函数后面的所有代码都不再执行,但它之前的代码当然是可以被执行的。
path是包括执行文件名的全路径名file既可是全路径名也可是可执行文件名
arg是可执行文件的全部命令行参数,可以用多个,注意最后一个参数必须为NULL。
argv是一个字符串的数组char*argv[]={“fullpath”,”param1”,”param2”,...NULL};
envp指定新进程的环境变量char*envp[]={“name1=val1”,”name2=val2”,...NULL};
exec函数族的参数传递有两种方式:
一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。
在这里是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,其语法为char*arg;字母为“v”(vertor)的表示将所有命令行参数整体构造指针数组传递,其语法为char*constargv[]。
以字母p结尾的函数通过搜索系统PATH这个环境变量来查找新程序的可执行文件的路径。
如果可执行程序不在PATH定义的路径中,我们就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数。
对有参数envp的函数调用,其以字母e结尾,函数通过传递envp传递字符串数组作为新程序的环境变量。
新进程中的全局变量environ指针指向的环境变量数组将会被envp中的内容替代。
注意:
对于有参数envp的函数,它会使用程序员自定义的环境变量,如果自定义的环境变量中包含了将要执行的可执行程序的路径,那么第一个参数中是不是我们就可以不用写全路径了呢?
不是的,必须写全路径。
因为我们自定义的环境变量不是用来寻找这个可执行程序的,而是在这个可执行程序运行起来之后给新进程用的。
可以用env命令查看环境变量。
execl示例:
#include
#include
intmain()
{
printf("aaaa\n");
execl("/bin/ls","ls","-l",NULL);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
execlp示例:
(在上面的基础上去掉了ls的路径/bin/)
#include
#include
intmain()
{
printf("aaaa\n");
execlp("ls","ls","-l",NULL);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
execv示例:
(把"ls","-l",NULL这些命令行参数通过指针数组str传给exec函数)
#include
#include
intmain()
{
char*str[]={"ls","-l",NULL};
printf("aaaa\n");
execv("/bin/ls",str);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
execvp示例:
(在上面的基础上去掉了ls的路径/bin/)
#include
#include
intmain()
{
char*str[]={"ls","-l",NULL};
printf("aaaa\n");
execvp("ls",str);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
execle示例:
#include
#include
intmain()
{
char*arrEnv[]={"PATH=/bin:
/usr/bin","TERM=console",NULL};
printf("aaaa\n");
execle("/bin/ls","ls","-l",NULL,arrEnv);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
execve示例:
#include
#include
intmain()
{
char*str[]={"ls","-l",NULL};
char*arrEnv[]={"PATH=/bin:
/usr/bin","TERM=console",NULL};
printf("aaaa\n");
execve("/bin/ls",str,arrEnv);
printf("bbbb\n");
printf("bbbb\n");
return0;
}
其它示例:
get_arg.c测试environ,用来输出本进程默认的环境变量。
#include
#include
#include
externchar**environ;//声明该变量
intmain(intargc,char*argv[])
{
inti=0;
for(;iprintf("arg%d:
%s\n",i,argv[i]);
char**ppEnv=environ;
while(ppEnv&&*ppEnv)
printf("%s\n",*ppEnv++);
return0;
}
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件,如果不是可以执行的文件,那么就解释成为一个shell文件,sh**执行!
上面6条函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。
参数argc指出了运行该程序时命令行参数的个数,数组argv存放了所有的命令行参数,数组envp存放了所有的环境变量。
环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境