linux系统-进程管理

进程控制

fork函数

创建一个子进程, 原型:

1
2
3
4
5
6
7
pid_t fork();		/*函数原型相当简单:空参,返回一个整数pid*/

//返回值:
// 返回值有两个:父进程的fork返回子进程的id,子进程返回0(表示fork成功)
// 返回子进程的pid(一个非负整数>0)
// 返回0
// 失败返回-1并设置errno

fork确实创建了一个子进程并完全复制父进程,但是子进程是从fork后面那个指令开始执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>


int main()
{
pid_t pid;
printf("sdsfdfdsfd\n");
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
printf("I'm child, pid =%u,ppid = %u\n",getpid(),getppid());
}
else
{
printf("I'm Parent, pid =%u,ppid = %u\n",getpid(),getppid());
}
printf("yyyyyyy\n");
return 0;
}

创建n个线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
int i;
pid_t pid;
printf("xxxxxxx\n");

for(i=0; i<5 ;i++)
{
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
break;
}
}
if(i < 5)
{
sleep(i);
printf("I'm %d child, pid = %u\n",i+1, getpid());
}
else
{
sleep(i);
printf("I'm parent\n");
}
return 0;
}

父进程和子进程谁先执行?谁先抢到cpu就谁先执行。如果不加sleep,则输出是乱序的(反映了操作系统对进程调度的无序性)。

getuid

  • 获取当前进程实际用户ID: uid_t getuid(void)

  • 获取当前进程有效用户ID: uid_t geteuid(void)

getgid

  • 获取当前进程使用用户组ID: gid_t getgid()

  • 获取当前进程有效用户组ID: gid_t getegid()

进程共享

父子进程之间在fork后,有哪些相同,哪些相异?

刚fork之后:

父子相同之处:全局变量、.data段,.text段,栈,堆,环境变量,用户ID,宿主目录,进程工作目录,信号处理方式…

父子不同之处:进程ID,fork返回值,父子进程ID,进程运行时间,闹钟(定时器),未决定信号集

注意:子进程并不是把父进程0~3G地址空间完全cpoy一份, 然后映射到物理内存。 父子进程之间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int var = 34;
int main()
{
pid_t pid;

pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid > 0)
{
sleep(2);
// var = 55;/*读时共享,写时复制*/
printf("I'm parent pid = %d, parentID = %d, var = %d\n",getpid(), getppid(),var);
}
else if(pid == 0)
{
var = 100;
printf("child pid = %d, parentID = %d, var = %d\n",getpid(),getppid(),var);
}
printf("var = %d\n",var);

return 0;
}

重点:

  • 躲避父子进程共享全局变量的误区(线程之间可以共享全局变量)。

  • 父子进程共享

    • 文件描述符(打开文件的结构体)
    • mmap建立的映射区(进程间通信)

特别:fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。

gdb调试

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过命令设置gdb调试工具跟踪父进程或是子进程。默认跟踪父进程。

1
2
set follow-fork-mode child #命令设置gdb在fork之后跟踪子进程
set follow-fork-mode parent #设置跟踪父进程

注意:一定要在fork函数调用之前设置才有效。

exec函数族

fork函数创建子进程后执行的是和父进程相同的程序(但可能执行不同的代码分支)子进程往往要调用一种exec函数以执行另一个程序当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

将当前进程的.text, .data替换为所要加载的程序的.text, .data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。

  • int execl()
  • int execlp()
  • int execle()
  • int execv()
  • int execvp()
  • int execve()

execlp

execlp中的p表示环境变量, 所以该函数通常用来调用系统程序

1
2
int execlp(const char* file, const char* arg, ... /* (char  *) NULL */);
/*参数1:要加载的程序的名字的,该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后但没有参数1则出错返回。*/

注意结尾加上NULL指定变参结束, printf函数也是变参, 结尾也要加上NULL作为哨兵.

该函数通常用来调用系统程序。如ls, cat , date等命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc,char* argv[]){

pid_t pid=fork();
if(pid==-1)
perr_exit("fork error");

if(pid==0){
/*参数从argv[0]开始算*/
execlp("ls","ls","-l","-R","-h",NULL);
/*正常情况下是不会执行到这里的,只有当出错时才会返回到这里执行*/
perror("execlp error");
exit(1);
}else if(pid>0){
printf("I'm parent:%d\n",getpid());
sleep(1);
}
return 0;
}

先fork, 再exec, 这就是bash的大概原理.

如果要执行自己的可执行文件:

1
execl("./test","./test",NULL);

execl

加载一个进程,通过 路径+程序名 来加载。

1
2
3
4
5
int execl(const char *path, const char\* arg, ...)

//对比execlp
execlp("ls","ls","-a","-l",NULL);
execl("/bin/ls","ls","-a","-l");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//exec_ps.c
int main(int argc,char* argv[]){

int ret=0;
int fd1=0;
/*打开或创建一个文件*/
fd1=open("ps.log",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1==-1)
perr_exit("open error");
/*将STDOUT_FILENO指向fd1*/
ret=dup2(fd1,STDOUT_FILENO);
if(ret==-1)
perr_exit("dup2 error");
/*执行命令*/
execlp("ps","ps","aux",NULL);
perror("execlp error");
exit(1);
return 0;
}

exec函数族一般规律

exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常直接在exec函数后直接调用perror()和exit(),无需if判断。

  • l (list) 命令行参数列表

  • p (path) 搜索file时使用path变量

  • v (vector) 使用命令行参数数组

  • e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量。

事实上,只有execve是真正的系统调用,其他五个函数最终都调用execev,所以execve在man手册第二节,而其他函数在man手册第三节。

回收子进程

孤儿进程

父进程先于子进程结束,则子进程变为孤儿进程,子进程的父进程变为init进程,称为init进程领养孤儿进程。该过程主要是为了后期进行回收。

僵尸进程

进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸(Zombie)进程。

特别注意:僵尸进程不能使用kill命令来清除。因为kill命令只是用来终止进程的,而僵尸进程已经终止。

进程的运行状态:R 运行, S 后台运行, Z 僵尸进程

用什么办法可以清除僵尸进程?避免僵尸进程:回收

wait函数

一个进程在终止的时候会关闭所有的文件描述符,释放在该用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息。如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。一个进程的退出状态可以在shell中用特殊变量$?查看,因为shell是它的父进程,当它终止时shell调用waitwaitpid得到它的退出状态同时彻底清除掉这个进程。

父进程调用wait函数可以回收子进程终止信息,该函数的三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)
1
2
pid_t wait(int *status);
//成功:清理掉的子进程ID, 失败:-1(没有子进程)

当进程终止时,操作系统的隐式回收机制会:

  • 关闭所有文件描述符
  • 释放用户空间分配的内存,内核的PCB仍存在。其中保存该进程的退出状态。(正常终止:退出值;异常终止:终止信号)

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可以分为三组:

1
2
3
4
5
6
7
8
9
WIFEXITED(status) //为非0 :进程正常结束
WEXITSTATUS(status) //如上宏为真,使用此宏:获取进程退出状态(exit的参数)

WIFSIGNALED(status) //为非0: 进程异常终止
WTERMSIG(status) //如上宏函数为真,使用此宏:取得使进程终止的那个信号的编号

WIFSTOPED(status) //为非0:进程处于暂停状态
WSTOPSIG(status) //加上宏为真,使用此宏:取得使进程暂停的那个信号的编号
WIFCONTINUED(status) //为真,进程暂停后已经继续运行

kill -l 可以查看进程结束的所有状态

程序所有异常终止的原因都是因为信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc,char* argv[]){
pid_t pid,wpid;
int status=0;

pid=fork();
if(pid==-1)
perr_exit("fork error");

if(pid==0){
printf("I'm child:%d,my parent is %d,I'm going to sleep 10s\n",getpid(),getppid());
sleep(10);
printf("I'm child,I'm going to die\n");
return 73;
}else if(pid>0){
//wpid=wait(NULL); //不关心子进程退出原因
wpid=wait(&status);
if(wpid==-1)
perr_exit("wait error");
/*如果子进程正常终止,则可获取它的退出值*/
if(WIFEXITED(status))
printf("My child exited with:%d\n",WEXITSTATUS(status));
/*如果子进程被信号终止,可获取结束它的信号*/
else if(WIFSIGNALED(status))
printf("My child killed by:%d\n",WTERMSIG(status));
/*提示回收完成*/
printf("I'm parent,wait %d finish\n",wpid);
}
return 0;
}

waitpid函数

作用同wait,但可指定pid进程清理,可以不阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
pid_t waitpid(pid_t pid, int *status, int options); 
//成功:返回清理的子进程ID, 失败:-1(无子进程)

/*特殊参数和返回情况:
参数pid:
>0 回收指定ID的子进程
-1 回收任意子进程(相当与wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程(进程组号取反, 表示回收指定进程组的任意子进程)

参2传进程结束状态, 如果不关心直接传NULL(传出参数)

参3传回收方式:WNOHANG(非阻塞)*/

注意:一次waitwaitpid调用只能清理一个子进程,清理多个子进程应该使用循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//带有bug的版本
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
for(i=0;i<5;++i){
if(fork()==0){
if(i==2)
pid=getpid();
break;
}
}

if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(pid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}

bug的原因:在fork()==0时是在子进程的执行逻辑中保存了pid, 但是子进程执行结束后直接返回, 用户空间的地址空间被回收, 当然也就没有了pid这个变量, 所以后面父进程waitpid时拿到的pid一直是0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//修改bug后
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;
int temppid=0;

for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
/*在父进程中,如果i==2,将fork的返回值存入temppid*/
if(i==2)
temppid=pid;
}

if(i==5){
//wpid=waitpid(-1,NULL,WNOHANG); //以非阻塞的方式回收任意子进程
sleep(5);
wpid=waitpid(temppid,NULL,WNOHANG);
if(wpid==-1)
perr_exit("waitpid error");
printf("I'm parent,wait a child finish:%d\n",wpid);
}
else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}

sleep(1);
return 0;
}

回收多个子进程: 用while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc,char* argv[]){
int i=0;
int wpid=0;
int pid=0;

for(i=0;i<5;++i){
pid=fork();
if(pid==0)
break;
}

if(i==5){
/*以非阻塞忙轮询的方式回收子进程*/
while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
if(wpid>0)
printf("wait chile:%d\n",wpid);
else if(wpid==0)
sleep(1);
}
}else{
sleep(i);
printf("I'm %dth child,my pid=%d\n",i+1,getpid());
}
sleep(1);
return 0;
}

wait/waitpid只能回收子进程, 爷孙的也不行.