0%

unix环境高级编程 - 进程控制

最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。

本系列博文均根据学习《UNIX环境高级编程》一书总结而来;

运行环境:

  • 操作系统: ubutnu 16.04
  • 编译器:QtCreator CLion 2020.3

进程控制

进程概念

  • 程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间, 是一个静态的概念.

  • 进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念

  • 程序 → 剧本(纸)

  • 进程 → 戏(舞台、演员、灯光、道具…)

同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)

并行和并发

  • 并发,在一个时间段内, 是在同一个cpu上, 同时运行多个程序。

如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。

PCB

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。

/usr/src/linux-headers-4.4.0-96/include/linux/sched.h文件的1390行处可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。

  • 进程的状态,有就绪、运行、挂起、停止等状态。

  • 进程切换时需要保存和恢复的一些CPU寄存器。

  • 描述虚拟地址空间的信息。

  • 描述控制终端的信息。

  • 当前工作目录(Current Working Directory)。

    • getcwd –pwd
  • umask掩码。

  • 文件描述符表,包含很多指向file结构体的指针。

  • 和信号相关的信息。

  • 用户id和组id。

  • 会话(Session)和进程组。

  • 进程可以使用的资源上限(Resource Limit)。

    • n ulimit -a

进程状态

创建进程

fork

fork返回值,父进程返回的是子进程的PID,这个值大于0,子进程返回0;

父进程执行PID大于0,子进程执行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
33
34
35
36
/**
* @file 创建子进程
*
* 示例程序 - 01_fork.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
int main()
{
printf("before fork, PID == %d\n",getpid());
//pid_t fork(void);
pid_t pid = fork(); //创建子进程
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father Process PID == %d\n",getpid());
sleep(1);
}
else if(pid == 0) //子进程
{
printf("child Process PID == %d\n",getpid());
}
printf("after fork PID == %d\n",getpid()); //父子公共代码,都拥有执行时间
return 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* @file 创建子进程
*
* 示例程序 - 01_fork.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>


int main(int argc,char *argv[])
{
int i = 0;
for (; i < 3; i++) {
pid_t pid = fork();
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());
sleep(1);
}
else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
break;
}
}

if(i == 0) //FirstProcess
{
printf("[%d]:child---pid:%d\n",i,getpid());
}
if(i == 1) //FirstProcess
{
printf("[%d]:child---pid:%d\n",i,getpid());
}
if(i == 2) //FirstProcess
{
printf("[%d]:child---pid:%d\n",i,getpid());
}
if(i == 3) //FirstProcess
{
printf("[%d]:child---pid:%d\n",i,getpid());
}
sleep(10);
return 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
33
34
35
36
37
38
39
40
41
42
43
/**
* @file 验证父子进程共享全局变量
*
* 示例程序 - 01_fork.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

int g_var = 99;

int main()
{
pid_t pid = fork(); //创建子进程
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());
g_var--; //全局变量--
printf("g_var = %p\n",&g_var);
printf("g_var = %d\n",g_var);
sleep(1); //避免父进程没有执行,子进程结束
}
else if(pid == 0) //子进程
{
//sleep(1);
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
printf("g_var = %d\n",g_var);
printf("[g_var] = %p\n",&g_var);
}
return 0;

}

读时共享,写时复制

PS 和 KILL

1
2
3
ps -ajx
ps -ef | grep root
kill -9 进程名

exec函数族

有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

execl

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
36
37
38
39
40
41
42
/**
* @file execl函数使用拉起自己的程序
*
* 示例程序 - 04_execl.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>


int main(int argc,char *argv[])
{
pid_t pid = fork(); //创建子进程
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());
sleep(1); //避免父进程没有执行,子进程结束
}
else if(pid == 0) //子进程
{
//sleep(1);
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
//execl("/bin/ls","ls","-l",NULL);
//execl("./main","main","Hello","World","sYstemkl1t",NULL);
//execlp("ls","ls","-l",NULL); //拉起ls命令
execlp("./main","main","Hello","World","sYstemkl1t",NULL);
perror("main Error");
}
return 0;

}

进程回收

孤儿进程

  • 孤儿进程的概念:

    ​ 若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。

  • 为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。

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
36
37
38
39
40
/**
* @file 孤儿进程验证
*
* 示例程序 - 05_oprhan.c
*
* @author Steve & sYstemk1t
*
*/


#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>


int main()
{
pid_t pid = fork(); //创建子进程
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
sleep(5);
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());

}
else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
sleep(20);
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
}
return 0;

}

父进程先退出,子进程就变成了孤儿进程,此时被init领养;当孤儿进程退出后,就会被init进程回收

僵尸进程

子进程先退出,父进程没有完成对子进程的回收,此时子进程就称为僵尸进程

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
36
37
38
39
/**
* @file 僵尸进程验证
*
* 示例程序 - 06_zomble.c
*
* @author Steve & sYstemk1t
*
*/


#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>


int main()
{
pid_t pid = fork(); //创建子进程
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
sleep(200);
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());

}
else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());

}
return 0;

}

如果要杀死僵尸进程,那么应该使用杀死僵尸进程的父进程的方法来解决僵尸进程;

回收函数

wait

1
pid_t wait(int *status);
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @file 回收函数wait使用
*
* 示例程序 - 07_wait.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
int i = 0;
int status;
pid_t pid = fork();
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father [%d] Process PID == %d Process PPID = %d\n",pid,getpid(),getppid());
pid_t wpid = wait(&status);

printf("wpid == %d\n",wpid);
if (WIFEXITED(status)) //标识正常退出
{
printf("child normal exit status == [%d]\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)) //被信号杀死
{
printf("child killed by signal, signo == [%d]\n",WTERMSIG(status));
}

}
else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
sleep(20);
return 9;
}
return 0;
}

waitpid

1
2
3
#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)

pid

从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  1. pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  2. pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
  3. \pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。**
  4. pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

\options**

options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用”|”运算符把它们连接起来使用;

如果使用了WNOHANG(wait no hung)参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @file 回收函数waitpid使用
*
* 示例程序 - 08_waitpid.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
int i = 0;
int status;
pid_t pid = fork();
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father [%d] Process PID == %d Process PPID = %d\n", pid, getpid(), getppid());

while (1) {
pid_t wpid = waitpid(-1, &status, WNOHANG); //-1标识等任意子进程
printf("wpid == %d\n", wpid); //测试用
if (wpid > 0) {
if (WIFEXITED(status)) //标识正常退出
{
printf("child normal exit status == [%d]\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) //被信号杀死
{
printf("child killed by signal, signo == [%d]\n", WTERMSIG(status));
}
}
else if (wpid == 0) {
printf("child is living = %d\n", wpid);
}
else if (wpid == -1) { //没有子进程
printf("No child is livint = %d\n", wpid);
break;
}
}
}


else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
sleep(2);
return 9;
}
return 0;
}