Linux 进程控制编程
1.进程
a.进程是什么?
Linux是一个多任务的操作系统,也就是说,在同一个时间内可以有多个进程同时执行。那么,Linux是如何实现多进程同时执行的呢?Linux 使用了一个被称为“进程调度”的方式。首先,为每个进程指派一定的运行时间,通常以毫秒为单位,然后依照某种规则,从众多的进程中挑选一个运行,在此时间内,其他进程为等待状态。当正在运行的程序时间耗尽,或者运行结束,或者因为某种原因暂停,Linux会重新调度,挑选下一个进程运行。
b.进程的分类
进程一般有交互进程,批处理进程和守护进程三大类,其中,守护进程是最活跃的,一般在后台运行,一般是由系统在开机时通过脚本自动激活启动或者root用户启动的。
举个例子:在Fedora或Redhat中,我们可以定义httpd服务器的启动脚本的运行级别,此文件位于/etc/init.d目录下,文件名是httpd,他是httpd服务器的守护程序,若把他的运行级别设置为3或者5时,当系统启动时,它也会跟着启动。
[root@localhost ~]#chkonfig - - level 35 httpd on
由于守护进程是一直运行的,所以它的状态是等待请求处理任务。
c.进程的属性
- 进程ID(PID):是唯一的数值,用来区分进程
- 父进程和父进程的ID(PPID)
- 启动进程的用户ID(UID)和所归属的组(GID)
- *进程状态:运行R,休眠S,僵尸Z
- 进程执行的优先级
- 进程所连接的终端名
- 进程资源占用(内存、CPU占用量)
d.父进程和子进程
首先,父进程和子进程是管理和被管理的关系,当父进程终止时,子进程也随之终止;但子进程终止,父进程不一定会终止。
2.Linux进程管理
a.ps监视进程工具
ps的参数说明
字母 | 说明 |
---|---|
l | 长格式输出 |
u | 按用户名和启动顺序来显示进程 |
j | 用任务格式来显示进程 |
f | 用树状格式来显示进程 |
a | 显示所有用户的所有进程(包含其他用户) |
x | 显示无控制终端的进程 |
r | 显示运行中的进程 |
ww | 避免详细参数被截断 |
我们常用到的选项组合是aux或lax,另外还有参数f的应用。
字母 | 说明 |
---|---|
ps | aux或lax输出的解释 |
USER | 进程的属主 |
PID | 进程的ID |
PPID | 父进程 |
%CPU | 进程占用的CPU百分比 |
%MEM | 占用内存的百分比 |
NI | 进程的NICE值,数值越大,表示越少占用CPU |
VXZ | 进程虚拟大小 |
RSS | 驻留中页的数量 |
TTY | 终端ID |
STAT | 进程状态 |
D | 不可中断 |
R | 正在运行,或在队列中的进程 |
S | 处于休眠状态 |
T | 停止或被追踪 |
W | 进入内存交换(内核2.6版本以后无效) |
X | 死掉的进程 |
Z | 僵尸进程 |
< | 优先级高的进程 |
N | 优先级较低的进程 |
L | 有些页被锁进内存 |
s | 进程的领导者(在他之下有子进程) |
WCHAN | 正在等待的进程资源 |
START | 启动进程的时间 |
TIME | 进程消耗CPU的时间 |
COMMAND | 命令的名称和参数 |
+ | 位于后台的进程组 |
b.pgrep查询进程的工具
pgrep是通过程序的名字来查询进程的工具,一般用来判断程序是否正在运行。
其用法是:
#ps 参数选项 程序名
-l : 列出程序名和进程ID
-o : 进程起始的ID
-n : 进程终止的ID
c .终止进程的工具
终止一个正在运行的程序或者终止一个进程,一般通过kill、killall、pkill、xkill
等进行的。
需要注意的是如果使用以上工具终止涉及数据库服务程序的父进程时,会让数据库产生更多的文件碎片,当碎片达到一定程度时,数据库就会有崩溃的危险。
举个例子:
mysql服务器最好是按照正常的程序关闭,而不是用pkill mysqld或者killall mysqld这样危险的动作,当然对于占用资源过多的数据库子进程,我们应该用kill来杀掉。
1.kill函数
kill是和ps或pgrep命令结合一起使用的:
kill [信号代码] 进程ID
注:信号代码可以省略,我们通常使用的是-9,表示强制终止。
2.killall函数
killall可以通过程序的名字直接杀死所有进程,其用法为:
killall 正在运行的程序名
killall是和ps或pgrep命令结合一起使用的。
3.pkill函数
和killall用法差不多,也是杀死进程中的程序,如果想杀死单个的进程,请用kill
pkill 正在运行的程序名
4.xkill函数
用于在桌面上杀死图形界面的程序,
例如:当firefox崩溃不能退出时,单击鼠标就能杀死firefox。在xkill运行时会出现人脑骨类似的图标,那个程序崩溃一点就OK了
d.top监视系统任务的工具
与ps相比,top是动态监视系统任务的工具,top输出的结果是连续的。
top的调用方法如下:
top 选择参数
参数描述如下:
字母 | 说明 |
---|---|
-b | 以批量模式运行,但不能接受命令输入。 |
-c | 显示命令行,而不仅仅是命令名 |
-d N | 显示两次刷新时间的间隔,-d 5,表示两次刷新间隔为5s |
-i | 禁止显示空闲进程或僵尸进程 |
-n NUM | 显示或更新次数然后退出,-n 5,表示更新5次数据就退出 |
-p PID | 仅监视指定进程的ID,PID是一个数值 |
-q | 不经任何延时就刷新 |
-s | 安全模式下运行,禁用一些命令 |
-S | 累积模式,输出每个进程的总的CPU时间,包括已死的子进程 |
交互式命令键位如下所述
字母 | 说明 |
---|---|
space | 立即更新 |
c | 切换命令名显示,或显示整个命令(包括参数) |
f, F | 增加显示字段,或删除显示字段 |
h, ? | 显示有关安全模式及累积模式的帮助信息 |
k | 提示输入要杀死的进程ID,目的是用来杀死该进程(默认信号为15) |
i | 禁止空闲进程和僵尸进程 |
l | 切换到显示负荷平均值和正常运行的时间等信息 |
m | 切换到内存信息,并以内存占用大小排序 |
n | 提示显示进程数,如输入3,就在整屏上显示3个进程 |
o、O | 改变显示字段的顺序 |
r | 把renice应用到一个进程,提示输入PID和renice的值 |
s | 改变两次刷新时间间隔,以秒为单位 |
t | 切换显示进程和CPU状态的信息 |
A | 按进程生命大小排序,由大到小 |
M | 按内存占用大小排序,由大到小 |
N | 以进程ID大小排序,由大到小 |
S | 切换到累积时间模式 |
T | 按时间/累积时间对任务进行排序 |
W | 把当前的配置写到”~/.toprc”中 |
e.进程的优先级
在Linux系统中,进程之间是竞争资源关系,这个竞争关系通过一个数字来实现,也就是谦让度,高谦让度表示进程优先级最低,负值或0值表示最高优先级,谦让度值的范围为-20~19。
nice可以为进程指定谦让度的值,进程优先级的值是父进程shell优先级的值与我们所指定谦让度的相加结果。我们在使用nice设置程序的优先级所指定数值是一个增量,并不是优先级的绝对值。
nice -n 谦让度的增量值 程序
[root@localhost~]#nice -n 5 gaim &
运行gaim程序,并为它指定谦让度增量为5。
renice通过进程ID(PID)来改变谦让度,进而达到更改进程优先级
renice 谦让度 PID
[root@localhost~]#ps lax | grep gaim
4 0 4437 3419 10 -5 120924 20492 - S< pts/0 0:01 gaim
0 0 4530 3419 10 -5 5160 708 - R<+ pts/0 0:00 grep gaim
[root@localhost~]#renice -6 4437
4437: old priority -5, new priority -6
[root@localhost~]#ps lax | grep gaim
4 0 4437 3419 14 -5 120924 20492 - S< pts/0 0:01 gaim
0 0 4534 3419 11 -5 5160 708 - R<+ pts/0 0:00 grep gaim
3.Linux进程的三态
a.三种基本状态
执行状态(Ready):进程正在占用CPU
就绪状态(Running):进程已具备一切条件,正在等待分配CPU的处理时间片
等待状态(Blocked):进程不能使用CPU,若等待事件发生则可将其唤醒
b.三种状态的转换
4.Linux进程结构
Linux的一个进程在内存里有三部分数据——数据段、堆栈段和代码段。代码段存放程序代码的数据,假如机器中有数个进程运行同一程序,那么它们就可以使用同一个代码段。堆栈段存放时子程序的返回地址,子程序的参数,以及程序的局部变量。数据段存放程序的全局变量、常数以及动态数据分配的数据空间,其中堆是用户分配(malloc),栈是系统分配(int a)。系统如果同时运行数个相同的程序,他们之间就不能使用同一个堆栈段和数据段。
5.Linux进程控制块PCB
Linux控制块是一个由结构task_struct所定义的数据结构,task_struct存放在/include/linux/sched.h中,包括管理进程的各种信息。
Linux系统的PCB包括很多参数,每个PCB约占1KB的内存空间。
6.Linux进程调度
a.调度目标
CPU的调度就是要达到极小化平均响应时间、极大化系统吞吐率、保持系统各个功能部件处于繁忙状态和提供某种公平的机制。
对于实时系统来说,调度目标就是要达到截止时间前完成所应该完成的任务和提供性能的可预测性。
b.调度的算法
1.先来先服务调度算法
2.短进程优先调度算法
3.高优先级优先调度算法
4.时间片轮转法
c.优先级反转
优先级翻转(Priority Inversion),是指某同步资源被较低优先级的进程/线程所拥有,较高优先级的进程/线程竞争该同步资源未获得该资源,而使得较高优先级进程/线程反而推迟被调度执行的现象。
优先级继承(Priority Inheritance),当一个高优先级进程等待一个低优先级进程持有的资源时,低优先级进程将暂时获得高优先级进程的优先级别。在释放完共享资源后,低优先级进程回到了原来的优先级别。
死锁,两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
7.进程的创建
a.获取进程
每个进程都对应一个ID,我们可以通过系统调用getpid得到,而getppid则可以得到父进程的ID。
格式如下
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void) 获取本进程ID
pid_t getppid(void) 获取父进程ID
返回值为进程号。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
printf( "PID = %d\n", getpid() );
printf( “PPID = %d\n”, getppid() );
return 0;
}
b.启动进程
fork,vfork函数都能用于创建子进程,但是fork和vfork还是有一些细微的差别。
fork()创建子进程时,子进程只是完全复制父进程的资源,复制出来的子进程又有自己的task_struct结构和PID,但却有父进程其他所有的资源。但由于现在的Linux采用copy-on-write(COW写时复制)的技术,为了降低开销,fork起初不会真的产生两个不同的复制,若后来真的发生了写入,那意味着父子进程数据不一致,于是产生复制行为,每个进程拿到了属于自己的那一份,降低了系统的开销。fork()调用一次返回两个值,对于父进程返回子进程的进程号;对于子进程返回0;如果调用出错,返回-1;在fork()之后的指令,父子进程都会执行,可以说,子进程是父进程的副本,它将获得父进程的数据空间、堆和栈的副本,但是父子进程并不共享这部分的内存。也就是说,子进程对父进程同名变量修改,并不会影响其在父进程中的值。
举个例子:
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
/*此时仅有一个进程*/
pid=fork();
/*此时已经有两个进程在同时运行*/
if(pid<0)
printf("error in fork!");
else if(pid==0)
printf("I am the child process, ID is %d\n",getpid());
else
printf("I am the parent process,ID is %d\n",getpid());
}
#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
int count=0;
pid = fork();
count++;
printf( “count = %d\n", count );
return 0;
}
输出:
count = 1
count = 1
vfork同上也会产生一个子进程,他会复制父进程的数据与堆栈空间,并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制等。子进程不会继承父进程的文件锁定和未处理的信号。
注意,Linux不保证子进程会比父进程先执行或晚执行,因此编写程序时要留意死锁或竞争条件的发生。
区别:
fork:子进程拷贝父进程的数据段
vfork:子进程与父进程共享数据段
即子进程修改父进程同名变量会使得该变量在父进程中也发生变化。
fork:父、子进程的执行次序不确定
vfork:子进程先运行,父进程后运行
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int count = 0;
pid = vfork();
if(-1 == pid)
{
perror("vfork");
exit(1);
}
else if(0 == pid)
{
count++;
printf("Child process pid is %d\n", getpid());
printf("count = %d\n", count);
exit(1);
}
else
{
count++;
printf("Parent process pid is %d\n", getpid());
}
printf("count = %d\n", count);
return 0;
}
输出:
count = 1
count = 2
在Linux系统调用中,还有一个exec族可以启动进程,exec族一共有6个:
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[]);
其中,只有execve才是真正的系统调用,其他函数都是在此基础上经过包装的库函数
execl函数
函数说明:execl()用来执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件是传递过去的argv[0], argv[1],…,最后一个参数必须用空指针NULL作为结束。
#include<unistd.h>
int main()
{
execl(“/bin/ls”,”ls”, “-al”,”/etc/passwd”,NULL);
return 0;
}
系统进程:system
system函数用于执行shell命令,例如
#include <stdio.h>
int main()
{
system("ls -al /etc/passwd /etc/shadow");
}
运行结果:
-rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd
-r-------- 1 root root 572 Sep 2 15 :34 /etc/shadow
8.进程等待
a.僵尸进程与孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
b.如何避免僵尸进程和孤儿进程
父进程可以通过wait,waitpid,signal函数来避免以上情况,其中,wait和waitpid是通过是将父进程挂起,等待子进程结束来避免的。而如果父进程不关心子进程何时结束,那么就可以使用“signal(SIGCHLD, SIG_IGN)”指令继续运行,子进程托由内核回收。还有一种方法,就是fork两次,具体可以参考以下链接:
僵尸进程和孤儿进程总结
c.wait和waitpid
c.1
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int * status)
功能:阻塞该进程,直到其某个子进程退出
c.2
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid (pid_t pid, int * status, int options)
功能:会暂时停止目前进程的执行,直到有信号来到或子进程结束
参数:如果不在意结束状态值,则参数status可以设成NULL。参数pid为欲等待的子进程识别码:
pid<-1 等待进程组识别码为pid绝对值的任何子进程。
pid=-1 等待任何子进程,相当于wait()。
pid>0 等待任何子进程识别码为pid的子进程。参数option可以为0 或下面的OR 组合WNOHANG:
如果没有任何已经结束的子进程则马上返回,不予以等待。
WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
返回值:如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno中。
wait用法举例:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pc,pr;
pc = fork();
if(pc == 0)
{
printf(“This is child process with pid of %d\n”,getpid());
sleep(10); /* 睡眠10秒钟 */
}
else if(pc > 0)
{
pr=wait(NULL); /* 等待 */
printf("I catched a child process with pid of %d\n"),pr);
}
exit(0);
}
9.进程退出
exit,_exit用于终止进程
区别:
_exit: 直接使进程停止,清除其使用的内存,并清除缓冲区中内容。
exit与 _exit的区别:在停止进程之前,要检查文件的打开情况,并把文件缓冲区中的内容写回文件才停止进程。
表头文件 | stdlib.h |
---|---|
定义函数 | void exit(int status); |
函数说明 | exit()用来正常终结目前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。 |
表头文件 | unistd.h |
---|---|
定义函数 | void _exit(int status); |
函数说明 | _exit()用来立刻结束目前进程的执行,并把参数status返回给父进程,并关闭未关闭的文件。 |
此函数调用后不会返回,并且会传递SIGCHLD信号给父进程,父进程可以由wait函数取得子进程结束状态。