首页 > 代码库 > 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