【Linux】第四章-进程控制

第四章 进程控制

进程创建

fork()

  fork()可以用于创建一个子进程,并且父进程返回其子进程的PID,子进程返回0。子进程会完全复制父进程的PCB并且会优先执行父进程。

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
/**
*
* fork初使用
* 通过复制调用进程,创建一个新的进程(子进程)
*/
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("parent pid = %d\n", getpid());
pid_t pid = fork();//复制了父进程的PCB,因此程序计数器也会跟父进程一致,因此子进程下一句执行的代码和父进程一致
printf("child1:%d\n", pid);
if(pid < 0)
{
return -1;
}
else if(pid == 0)//fork有返回值,子进程返回0,父进程返回的是子进程的pid
{
printf("child:%d\n", getpid());//得到当前进程的PID
}
else
{
printf("parent:%d\n", getpid());//得到当前进程的PID
}
printf("Misaki\n");
}


[misaki@localhost 第三章]$ ./Creat
parent pid = 6751
child1:6752
parent:6751
Misaki
child1:0
child:6752
Misaki

  在创建子进程时,系统会完全复制一份相同的物理内存,共用相同的代码,但是这样会极大的浪费内存,在有些情况下如果我们只是单纯的让子进程重复父进程执行的操作,我们完全不需要复制一份物理内存,因此写时拷贝技术就会帮上我们很大的忙,在创建子进程时,并不会立刻复制一份物理内存,会优先使用同一份物理内存,直到父进程或者子进程出现了数据或者代码的修改才会分配新的物理内存空间,这样可以大大的减少内存空间的占用。

  fork()创建的子进程会完全复制父进程的PCB但是完全复制一份PCB有时在对空间要求很严格的情况下会导致浪费很多的空间,因此我们就有了vfork()

vfork()

  vfork()函数的基本功能与fork()一致但有不同的地方是vfork()创建完子进程会优先执行子进程,并且子进程不会完全复制父进程的PCB,为了节省空间子进程会和父进程共用一块虚拟内存,为了不导致调用栈混乱,子进程会阻塞父进程,也就是说父进程会在子进程退出或替换后才会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>                                
#include <unistd.h>

int main()
{
pid_t pid = vfork();//创建子进程,阻塞父进程
while(1)
{
if(pid == 0)
{
//child
printf("----child!! pid:[%d]\n", getpid());
_exit(0);
}
if(pid > 0)
{
printf("----parent!! pid:[%d]\n", getpid());
}
sleep(1);
}
}

  vforkfork也会有调用失败的情况,比如说在系统中已经有了太多的进程或者用户的实际进程达到了上限,这样则系统不再允许我们创建新的进程。

进程终止

  进程退出往往分为以下几种方式,程序运行完毕正常退出和代码异常中止,我们平常使用的ctrl + c终止进程就是一种进程异常退出的方式,我们也可以通过调用接口和函数进行带退出状态的方式退出进程。

_exit()

  _exit()是一个函数,原型为:

        
void _exit(int status);

  可以让程序带状态进行退出,status是程序的退出状态,这个状态可以通过wait()函数接收到,也可以在命令行中通过echo $?命令进行查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
printf("fork error :%s\n", strerror(errno));
}
sleep(1);
_exit(-1);
}


[misaki@localhost process]$ echo $?
255

  明明我们返回了-1为什么终端中查看返回值却返回了255呢?

  因为status只有低8位可以被父进程查看到返回值信息,也就是说返回值信息一共只有255条,如果我们返回-1,实际上返回了255。

exit

  exit()也为退出函数,但是在退出进程前会对进程多很多的处理随后调用_exit()退出进程。这些退出前的处理包括:

  1、执行用户自定义的清理函数——atexiton_exit

  2、会关闭所有打开的流,所有的缓存数据都会被写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
exit1.c

#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Misaki");
exit(0);
}

exit2.c

#include <stdio.h>
#include <unistd.h>
int main()
{
printf("Misaki");
_exit(0);
}

[misaki@localhost process]$ ./exit2
[misaki@localhost process]$ ./exit1
Misaki[misaki@localhost process]$

  这个例子可以看出exit()退出进程会将缓冲区中的数据全部写入,但是_exit()却没有这些处理。

进程等待

  所谓进程等待是父进程等待子进程的终止并且得到子进程的退出信息,防止产生僵尸进程。进程等待是一种防止僵尸进程长生的重要方法,在合适的时间进行进程等待可以防止僵尸进程,确保不会产生内存泄漏,白白浪费资源。

wait()

  这个函数是最为基本的进程等待函数,原型为:

        
pid_t wait(int *status);

  返回等待到的子进程的pid,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
/*进程等待-避免产生建时进程*/                

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

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
sleep(5);
exit(1);
}
//pid_t wait(int *status);
// 等待任意一个子进程退出
// status:用于退出返回值
// 返回值:返回退出子进程的pid; 出错:-1
int status;
pid_t pid2 = wait(&status);
printf("pid = %d\n", pid2);
return 0;
}

[misaki@localhost process]$ ./wait
child = 4843
pid = 4843
status = 256

  这里我们已经等待到了我们的子进程退出,但是会发现返回的信息却不对,这是为什么呢?

  status的值不能单纯的看作是一个整形,而是应该用位图来理解。我们只讨论status的低16位。

  status的低8位用来保存异常终止信息,如果程序正常退出则低8位为0,否则会在低8位保存终止信号以及core dump标志及产生核心转储文件的标志。而高位才会保留我们自己返回的退出信息。并且C语言已经为我们实现准备好了宏来验证低7位是否为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
/*进程等待-避免产生建时进程*/                

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

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
exit(1);
}
//pid_t wait(int *status);
// 等待任意一个子进程退出
// status:用于退出返回值
// 返回值:返回退出子进程的pid; 出错:-1
int status;
pid_t pid2 = wait(&status);
printf("pid = %d\n", pid2);
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
//WIFSIGNALED()若WIFEXITED为真则提取子进程退出码。
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
return 0;
}


[misaki@localhost process]$ ./wait
child = 5529
pid = 5529
child exit code:1

  这样我们返回的信息码就被打印出来了。

waitpid()

  waitpid()wait()功能更加全面,它拥有更多的选项,可以在不阻塞父进程的情况下进行进程等待。原型:

        pid_t waitpid(pid_t pid, int *status, int options);

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
/*进程等待-避免产生建时进程*/           

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

int main()
{
int pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
//child
printf("child = %d\n", getpid());
sleep(5);
exit(1);
}
int status;
//pid_t waitpid(pid_t pid, int *status, int options);
// pid: 指定的进程id
// -1 等待任意子进程
// >0 等待指定子进程
// status: 用于获取返回值
// options:选项
// WNOHANG 将waitpid设置为非阻塞
// 返回值:<0:出错 ==0:没有子进程退出 >0: 退出子进程的pid
while (waitpid(pid, &status, WNOHANG) == 0) {
printf("no exit~~~smoking~~\n");
sleep(1);
}
//低8位为0则正常退出
//WIFEXITED()验证低8为是否为0,为0则为真
if (WIFEXITED(status)) {
printf("child exit code:%d\n", WEXITSTATUS(status));
}
//WIFSIGNALED()若WIFEXITED为真则提取子进程退出码。
if (WIFSIGNALED(status)) {
printf("exit signal:%d\n", WTERMSIG(status));
}
return 0;
}


[misaki@localhost process]$ ./wait
no exit~~~smoking~~
child = 6031
no exit~~~smoking~~
no exit~~~smoking~~
no exit~~~smoking~~
no exit~~~smoking~~
child exit code:1

  waitpid()可以不阻塞父进程,只等待一下如果当前没有子进程终止则便会继续执行父进程。

进程替换

  我们在创建子进程时现在往往只能让子进程执行和父进程一样的代码,这样我们想让子进程去执行另外的功能就需要用到进程替换。进程替换要用到exec系函数。我们先把他们的函数原型都先列出来再进行分析。

1
2
3
4
5
6
#include <unistd.h>
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[]);

  exec后跟不同的字符表示不同的功能:

  1、l:参数使用列表列出,结尾用NULL表示结束。

  2、p表示在环境变量中搜索参数,搜索到替换为环境变量中指定的文件。

  3、v表示参数用数组传递,数组最后一个元素用NULL表示结束。

  4、e表示需要自己传递环境变量。

  在系统中exec族函数最终都会调用execve,系统中只有这一个是系统接口,而其他函数不过是在execve上进行了一次封装。

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
exec.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
printf("Misaki\n");
execl("./env", "env", "-l", NULL);
return 0;
}

env.c:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[], char *env[])
{
int i;
for(i = 0; i < argc; i++) {
printf("argv[%d]=[%s]\n", i, argv[i]);
}
extern char **environ;
for (i = 0; environ[i] != NULL; i++) {
printf("env[%d] = [%s]\n", i, environ[i]);
}
char *ptr = getenv("MYENV");
printf("MYENV:[%s]\n", ptr);

return 0;
}


[misaki@localhost process]$ ./exec
Misaki
argv[0]=[env]
argv[1]=[-l]
env[0] = [XDG_SESSION_ID=1]
env[1] = [HOSTNAME=localhost.localdomain]
env[2] = [SELINUX_ROLE_REQUESTED=]
env[3] = [TERM=xterm]
env[4] = [SHELL=/bin/bash]
env[5] = [HISTSIZE=1000]
…………

  这样我们在exec.c中执行了进程替换代码后就用env.c中的代码替换了exec.c中剩下的代码,于是打印出了我们传入的参数和环境变量。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助