首页 > 代码库 > Linux进程控制知识总结
Linux进程控制知识总结
目录
一:进程标识符(ID)
二:进程操作
2.1创建一个进程
2.2 fork函数出错情况
2.3创建一个共享空间的子进程
2.4退出程序
2.5设置进程所有者
三:执行程序
3.1 exec函数
3.2 执行解释器文件
3.3在程序中执行Shell命令
四:关系操作符
4.1等待进程退出
4.2 等待指定的进程
进程控制
—— 一步
一:进程标识符(ID)
进程ID是用来标识进程的编号,就像身份证一样。不同的进程有不同的ID,可以通过ID来查询进程。进程标识符的类型是pit_t,其本质是一个无符号整型。使用ps命令可以查看当前进程。
每一个进程有6个重要的ID值:
进程ID;
父进程ID;
有效用户ID;
有效组ID;
实际用户ID;
实际组ID;
Linux下使用下列函数可以获得相应的ID:
#include<unistd.h>
pid_t getpid(void); //得到进程ID
pid_t getppid(void); //得到父进程ID
uid_t getuid(void); //得到实际用户ID
uid_t geteuid(void); //得到有效用户ID
gid_t getgid(void);
gid_t getegid(void);
上述几个函数成功则返回相应的ID,失败则返回-1。
示例:
/*打印相关进程信息*/
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid,ppid,uid,euid,gid,egid;
pid=getpid();
ppid=getppid();
uid=getuid();
euid=geteuid();
gid=getgid();
egid=getegid();
printf("id of current process:%u",pid);
printf("parent id of current process:%u",ppid);
printf("user of current process:%u",uid);
printf("effective user of current process:%u",euid);
printf("group id of current process:%u",gid);
printf("effective group id of current process:%u",egid);
return 0;
}
/*END*/
/*************************分隔符********************************/
二:进程操作
2.1创建一个进程
使用函数fork可以创建一个新进程:
#include<unistd.h>
pid_t fork(void);
返回值有3种情况:
1.对于父进程,fork函数返回新创建的子进程的ID;
2.对于子进程,fork函数返回0。由于系统的0号进程是内核进程,因此子进程的进程号不可能为0。由此区别父进程和子进程。
3.如果出错,返回-1。
fork创建的是子进程,其完全复制了父进程的地址空间的内容,包括堆栈段和数据段的内容。
子进程并没有复制代码,而是和父进程共用代码段。
简言之:子进程地址空间完全和父进程分开。父子进程是两个独立的进程,接收系统调用的机会相等。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int global; //全局变量,默认初值为0
int main()
{
pid_t pid;
int stack=1;
int *heap;
heap=(int *)malloc(sizeof(int));
*heap=2;
pid=fork(); //创建一个子进程
if(pid<0){
printf("fail to fork!\n");
exit(1);
}else if(pid==0){
global++;
stack++;
(*heap)++;
printf("the chid,global:%d,stack,%d,heap,%d\n",global,stack,*heap);
exit(0);
} //子进程运行结束
sleep(2);/*由于父子进程并列,被运行的几率相等,因此此处将父进程延时2s,以让子进程先运行*/
printf("the parent,global:%d,stack.%d,heap,%d\n",global,stack,*heap);
return 0;
}
/*END*/
运行结果:
the child,global:1,stack:2,heap:3
the parent,global:0,stack:1,heap:2
可见,在子进程里改变了变量的值,但并没有影响父进程变量的值。上面已经说过,子进程复制父进程地址空间里面的内容,即会将global,stack,heap复制到自己的地址空间,其地址空间是另外开辟的,与父进程的地址空间并列。因此子进程改变变量的值只是在自己的地盘改变,并不会影响到父进程地址空间
里的变量值。
2.2 fork函数出错情况
fork出错将返回-1,有两种情况可能导致fork函数出错:
1.系统中已经有太多的子进程存在;
2.调用fork函数的用户的进程太多了。
下面示例出错即是因为创建过多进程使系统奔溃:
示例:
#include<unistd.h>
#include<stdio.h>
int main()
{
while(1)
fork();
return 0;
}
/*END*/
程序将不断创建新的子进程,直至导致系统奔溃。
2.3创建一个共享空间的子进程
Linux下提供一个和fork函数类似的函数,也可以用来创建一个公用父进程地址空间的子进程,其函数原型如下:
#include<unistd.h>
pid_t vfork();
vfork与fork函数区别如下:
1.vfork函数产生的子进程和父进程完全共享地址空间,包括代码段,数据段和堆栈段,子进程对这些共享资源的修改可以影响到父进程。
2.vfork函数产生的子进程一定比父进程先运行,也就是说,父进程调用了vfork函数之后会等待子进程运行后再运行。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int global; //全局变量,默认初值为0
int main()
{
pid_t pid;
int stack=1;
int *heap;
heap=(int *)malloc(sizeof(int));
*heap=2;
pid=vfork(); //创建一个子进程
if(pid<0){
printf("fail to fork!\n");
exit(1);
}else if(pid==0){
global++;
stack++;
(*heap)++;
printf("the chid,global:%d,stack,%d,heap,%d\n",global,stack,*heap);
exit(0);
} //子进程运行结束
sleep(2);/*由于父子进程并列,被运行的几率相等,因此此处将父进程延时2s,以让子进程先运行*/
printf("the parent,global:%d,stack.%d,heap,%d\n",global,stack,*heap);
return 0;
}
/*END*/
运行结果:
the child,global:1,stack:2,heap:3
the parent,global:1,stack:2,heap:3
可见,子进程对变量的修改影响了父进程。这里有点类似指针的概念,子进程就像指向父进程的指针,其直接操作父进程地址空间里面的内容并且有效。
另外,值得注意的是,不要在任何函数(除主函数)的内部调用vfork,这样会导致段错误。(自己思考为何)
2.4退出程序
进程退出时需要用到退出函数:
#include<stdlib.h>
void exit(int status);
参数status表示退出状态,是个整值。利用exit(1),exit(2)...这样的设置可以方便我们调试程序。
另外Linux内核封装了_exit函数,它与exit的主要区别是:exit会做一些善后工作,比如清理I/O缓冲区,释放用户进程的地址空间等;而_exit函数则直接进入内核,释放用户进程的地址空间,所有用户空间的缓冲区内容都将丢失。
2.5设置进程所有者
Linux下可以改变进程用户的ID:
#include<unistd.h>
int setuid(uid_t uid); //修改实际用户ID和有效用户ID
int seteuid(uid_t uid); //只修改有效用户ID
成功返回0,失败返回-1。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
uid_t uid;
uid_t euid;
uid=getuid();
euid=geteuid();
printf("the uid is:%d\n",uid);
printf("the euid is:%d\n",euid);
if(setuid(1000)==-1)
{
printf("fail to set uid!");
exit(1);
}
printf("after changing:\n");
uid=getuid();
euid=geteuid();
printf("the uid is:%d\n",uid);
printf("the euid is:%d\n",euid);
return 0;
}
/*END*/
/*************************分隔符********************************/
三:执行程序
3.1 exec函数
下面我们来看看一个进程如何来启动另一个程序的执行。在Linux中要使用exec函数族。系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。exec函数族当然不止一个,但它们大致相同。下面对exec函数做简要介绍:
1)execl()
int execl(const char *path, const char *argv.......);
函数执行成功则不返回,否则返回-1
功能:execl()用于执行参数path字符串代表的文件路径,接下来参数代表执行文件时传递argv,最后一个参数必须以空指针结束。
2)execle()
int execle(const char *path, const char *argv.....,const char *envp[])
功能:执行那个参数path字符代表的文件路径,接下来参数代表执行文件时传递的参数argv,最后一个参数必须指向一个新的环境变量数组,成为新执行程序的环境变量。
3)execlp()
int execlp(const char *path, const char *arg......)
功能:从path环境变量所指目录中查找符合参数file的文件名,找到后执行该文件,接下来参数代表执行文件时传递的argv[0],最后一个参数必须以空指针NULL。
4)execv()
int execv(const char *path, const char *arg[])
功能:执行参数path字符代表的文件路径,第二参数以数组指针来传递给执行文件。
5)execve()
int execve(const char *filename, const char *argv[], const char *envp[])
功能:执行filename字符串代表的文件路径,中间参数利用数组指针来传递给执行文件,最后一个参数为传递给执行文件的新环境变量数组。
6)execvp()
int execvp(const char *filename, const char *argv[])
功能:从path环境变量所指定目录中查找符合参数file的文件名, 找到后执行此文件,第二个参数argv传递给要执行的文件。
当fork创建新进程时,即可使用exec函数执行新的程序,但并不是创建新的进程。一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
pid=fork(); //创建子进程
if(pid<0)
{
printf("fail to fork!");
exit(1);
}
else
if(pid==0) //子进程
{
/*调用exec函数,运行当前目录下的setuid程序*/
if(execvp("hello",NULL)==-1)
{
printf("fail to exec!\n");
exit(0);
}
/*这里应该永远不会执行,因为调用exec后这里的代码被setuid程序取代了*/
printf("the child is not hello!\n");
exit(0);//子进程正常退出
}
printf("the parent!\n"); //父进程
return 0;
}
/*END*/
其中hello为输出“hello world”的可执行程序。
运行输出:
hello world
the parent!
可见运行exec函数后的子进程将被加载的hello程序替换,直至子进程运行结束。
3.2 执行解释器文件
Linux中可执行文件分为两种,一种是二进制可执行文件,这种文件经过编译系统编译链接后生成。另一种是解释器文件,这种文件不是二进制的,而是一般的文本文件。这种文件起始形式为:
#!解释器程序的路径 解释器程序所需要的参数
例如Linux环境下使用的Shell脚本,其脚本的起始形式为:
#!/bin/sh
除去第一行以外的文件内容都被认为是解释器文件的内容,其处理交由定制的解释器程序处理。
此处只是粗略介绍,详细知识请查阅有关资料。
3.3在程序中执行Shell命令
Linux环境下使用system调用Shell指令:
#include<stdlib.h>
int system(const char *cmdstring);
下面是使用ls命令写到文件并读出显示的程序:
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#define MAX 1024
int main()
{
int fd,n;
char buf[MAX];
if(system("ls > temp.txt")==-1)
{
printf("fail to exec command!\n");
exit(1);
}
if((fd=open("temp.txt",O_RDWR))==-1)
{
printf("fail to open!\n");
exit(1);
}
if((n=read(fd,buf,MAX))==-1)
{
printf("fail to read!\n");
exit(1);
}
buf[n]=‘\0‘;
printf("%s",buf);
return 0;
}
/*END*/
该程序将ls命令执行结果写入temp.txt之中,之后再从temp.txt当中读取输到屏幕显示。其作用效果与直接执行ls命令一样。
system函数的实现:
system函数的执行流程分两步,首先调用system函数的进程创建出一个子进程,并调用wait函数等待子程序执行完毕;然后由这个子进程调用exec函数加载shell运行cmdstring中指定的命令,根据该流程可以编写一个system函数的实现原理性实现程序。
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
int sys(const char *cmdstring)
{
pid_t pid;
int status;
if(cmdstring==NULL) return 1;
pid=fork();
if(pid<0) status=-1;
else if(pid==0)
{
execl("/bin/sh","sh","-c",cmdstring,NULL);
_exit(127);
}
if(waitpid(pid,&status,0)==-1) status=-1;
return status;
}
/*END*/
/*************************分隔符********************************/
四:关系操作符
4.1等待进程退出
Linux下使用wait函数得到子进程结束的信息:
#include<sys/wait.h>
pid_t wait(int *statloc);
调用wait函数会使进程阻塞,直到该进程的任意一个子进程结束。statloc用来保存子进程的返回信息,内核将会取得的子进程结束信息保存在该指针所指向的空间。如果指针为NULL,则表示用户对返回信息并不关心。
返回信息是一个整数,不同的位代表不同的信息:正常结束状态,终止进程的信号编号和暂停进程的信号编号。Linux系统定义了专门的宏,用来判断哪种状态有效并取得相应的状态值。
状态 判断宏 取值宏
正常结束 WIFEXITED(status) WEXITSTATUS(status)
异常终止 WIFSIGNALED(status) WTERMSIG(status)
进程暂停 WIFSTOPPED(status) WSTOPSIG(status)
例如,当一个程序正常退出时,该进程父进程得到它返回信息,此时需要判断:如果WIFEXITED(status)为真,则该进程是正常退出,WEXITSTATUS(status)返回进程结束状态信息。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int status;
pid=fork();
if(pid<0){
printf("fail to fork!\n");
exit(1);
}else if(pid==0)
{
printf("exit normally!\n");
exit(0);
}
if(wait(&status)==-1){
printf("fail to wait!\n");
exit(1);
}
if(WIFEXITED(status)==1)
printf("the status is:%d\n",WTERMSIG(status));
return 0;
}
/*END*/
4.2 等待指定的进程
需要等待指定进程需要用到:
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *statloc,int options);
第一个参数为要等待子进程的ID,第三个是控制选项:
wait函数选项 选项说明
WCONTINUED 当子进程在暂停后继续执行,且其状态尚未报告,则返回其状态
WNOHANG 当等待的进程未结束运行时不阻塞,waitpid函数直接返回
WUNTRACE 当子进程暂停时,并且其状态自暂停以来还未报告过,则返回其状态
僵尸进程:
1). 僵尸进程概念:
就是已经结束了的进程,但是没有从进程表中删除。太多了会导致进程表里面条目满了,进而导致系统崩溃,倒是不占用系统资源。在进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。
除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会
一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
2).僵尸进程产生的原因:
每个 Linux进程在进程表里都有一个进入点(entry),核心程序执行该进程时使用到的一切信息都存储在进入点。当用ps命令察看系统中的进程信息时,看到的就是进程表中的相关数据。
当以fork()系统调用建立一个新的进程后,核心进程就会在进程表中给这个新进程分配一个进入点,然后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。当这个进程走完了自己的生命周期后,它会执行exit()系统调用,此时原来进程表中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程为止。由此可见,进程的出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。
3).僵尸进程的查看:
用 top命令,可以看到
Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
zombie前面的数量就是僵尸进程到数量;
ps -ef
出现:
root 13028 12956 0 10:51 pts/2 00:00:00 [ls] <defunct>
最后有 defunct的标记,就表明是僵尸进程。
4).僵尸进程解决办法:
4.1 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。 子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。
这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD 消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理 函数。
4.2 把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进 程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消 失。
kill -9 `ps -ef | grep "Process Name" | awk ‘{ print $3 }‘`
其中,“Process Name”为处于zombie状态的进程名。
4.3 杀父进程不行的话,就尝试用skill -t TTY关闭相应终端,TTY是进程 相应的tty号(终端号)。但是,ps可能会查不到特定进程的tty号,这时就 需要自己判断了。
4.4 实在不行,重启系统吧,这也是最常用到方法之一。
当一个进程已退出,但其父进程还没有调用系统调用wait(稍后介绍)对其进行收集之前的这段时间里,它会一直保持僵尸状态,利用这个特点,我们来写一个简单的小程序:
#include
#include
main()
{
pid_t pid;
pid=fork();
if(pid<0)
printf("error occurred!n");
else if(pid==0)
exit(0);
else
sleep(60);
wait(NULL);
}
/*END*/
sleep的作用是让进程休眠指定的秒数,在这60秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程60秒的僵尸状态。
编译这个程序:
$ cc zombie.c -o zombie
后台运行程序,以使我们能够执行下一条命令:
$ ./zombie &
[1] 1577
列一下系统内的进程:
$ ps -ax
... ...
1177 pts/0 S 0:00 -bash
1577 pts/0 S 0:00 ./zombie
1578 pts/0 Z 0:00 [zombie ]
1579 pts/0 R 0:00 ps -ax
看到中间的“Z”了吗?那就是僵尸进程的标志,它表示1578号进程现在就是一个僵尸进程。
参考文件:
吴岳等《Linux C程序设计大全》 清华大学出版社 2009.2
CSND博客
www.baidu.com