操作系统之进程控制
一、进程创建1、进程2、fork函数创建进程执行到内核中的fork代码时,内核做的操作。返回值3、常规用法4、代码5、执行结果二、进程终止1、进程的退出结果2、常见退出方法(1)正常终止(2)异常退出3、exit和_exit函数。区别4、代码5、运行结果三、进程等待(1)wait和waitpid(2)参数和返回值【1】pid【2】options【3】返回值(3)status【1】概念【2】分布【3
·
一、进程创建
1、进程
- 进程由内核数据结构和进程的代码和数据构成,内核数据结构由操作系统提供,进程的代码和数据一般从磁盘中加载,也就是程序加载之后的结果。
- 创建子进程时,代码一般是只能读取而不能被修改(写)的,所以父子进程可以共享代码;但数据可能会被修改,所以,会采用写时拷贝的方法,在修改的时候对父子进程的数据做分离操作。
2、fork函数
(1)概念
通过复制调用该函数的进程来创建新进程,新创建的流程称为调用该函数的进程的子进程,是调用该函数的进程的精确副本。
(2)创建进程执行到内核中的fork代码时,内核做的操作
- 分配新的内存块和内核数据结构给子进程。
- 将父进程部分数据结构内容拷贝至子进程。
- 将子进程添加到系统进程列表当中。
- fork函数返回,调度器开始调度。
(3)返回值
- 如果创建进程成功,子进程将会返回0,而父进程返回的是子进程的pid。
- 如果创建进程失败,则在父进程中返回-1,不会创建子进程,并正确地设置errno。
3、常规用法
- 父进程通过复制自己来创建子进程,父子进程在同一份代码中,同时执行不同的代码段。
- 父进程通过复制自己来创建子进程,父进程正常执行后续代码,子进程执行另一个不同的程序。
4、代码
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
printf("child, pid = %d, ppid = %d\n", getpid(), getppid());
}
else
{
printf("father, pid = %d, ppid = %d\n", getpid(), getppid());
sleep(2);
}
return 0;
}
5、执行结果
二、进程终止
1、进程的退出结果
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2、常见退出方法
(1)正常终止
- main函数返回
- 调用exit函数
- 调用_exit函数
(2)异常退出
- 程序执行时输入ctrl + c
- 信号终止
3、exit和_exit函数
(1)函数
- status定义了进程的终止状态。
(2)区别
- _exit函数是系统调用,而exit函数是库函数,即exit函数会调用_exit函数,只不过exit函数在调用前会执行一些操作。
4、代码
int sum(int top)
{
int sum = 0;
int i = 1;
for(i = 1;i <= top; ++i)
{
sum += i;
}
//exit(1);
return sum;
}
int main()
{
printf("hello snowdragon1\n");
printf("hello snowdragon1\n");
printf("hello snowdragon1\n");
int ret = sum(100);
printf("%d\n", ret);
printf("hello snowdragon2\n");
printf("hello snowdragon3\n");
printf("hello snowdragon4\n");
exit(1);
printf("hello snowdragon5\n");
printf("hello snowdragon6\n");
printf("hello snowdragon7\n");
return 0;
}
5、运行结果
- 下方为删除sum函数中exit函数的注释后的运行结果
三、进程等待
1、作用
- 当子进程退出后,如果父进程不管它,就可能造成僵尸进程问题,进而造成内存泄漏。而进程一旦变成僵尸状态,我们就无法通过信号之类的方法去杀死它,因为无法杀死一个已经死去的进程。
- 我们需要知道父进程派给子进程的任务完成的怎样,而当父进程进行进程等待时,如果子进程正常退出,父进程就能得知该类信息,获取子进程的退出信息。
- 父进程可以通过进程等待的方式,回收子进程资源。
2、方法
(1)wait和waitpid
(2)参数和返回值
【1】pid
- 当pid为-1时,表示等待任意一个子进程。此时,当waitpid函数的options为0时,其与wait函数的作用相同。
- 当pid大于0时,表示等待进程id与pid相等的子进程。
【2】options
- 当options为0时,表示父进程阻塞等待子进程。父进程一般是在内核中阻塞,等待被唤醒。
- 当options为WNOHANG时,若pid指定的子进程没有结束(退出),则waitpid函数返回0,不予以等待。若pid指定的子进程正常结束,则返回该子进程的id。
【3】返回值
- 当正常返回时,返回收集到的子进程的进程id。
- 如果waitpid函数设置了选项options为WNOHANG,而调用中waitpid函数发现没有已退出的子进程可收集,则返回0。
- 如果调用时出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
(3)status
【1】概念
status参数是一个输出型参数,由操作系统填充。如果传递给它的实参为NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
【2】分布
【3】两个宏
- WIFEXITED(status):表示子进程返回的状态,若子进程为正常终止,则为真,否则为假。
- WEXITSTATUS(status):若WIFEXITED非零(为真),则提取子进程的退出码。
3、代码
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("子进程%d, pid = %d, ppid = %d\n", cnt, getpid(), getppid());
sleep(1);
}
}
else
{
printf("父进程,pid = %d, ppid = %d\n", getpid(), getppid());
int status = 0;
waitpid(id, &status, 0);
//printf("等待子进程成功:%d, 子进程退出信号编号:%d,子进程退出码: %d\n", id, status & 0x7f, (status >> 8) & 0xff);
printf("等待子进程成功:%d, 子进程退出信号编号:%d,子进程退出码: %d\n", id, WIFEXITED(status), WEXITSTATUS(status));
}
return 0;
}
4、运行结果
5、使用wait和waitpid函数的原因
- 进程具有独立性,父子进程之间的数据会发生写时拷贝,如果采用全局变量之类的方法,父进程将无法拿到子进程的退出结果(status)。
- 僵尸进程至少会保留该进程的PCB信息,即task_struct里面会保留所属进程退出时的退出结果信息,而父进程调用wait和waitpid函数的本质是读取子进程的task_struct结构中两个保存退出信息的变量。
- 虽然进程具有独立性,而进程退出码是进程的数据。但是,wait和waitpid函数是系统调用。所以,父进程调用wait和waitpid函数后所进行的操作是由操作系统执行的。
四、进程程序替换
1、概念
- 程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据)到调用进程的地址空间中。
- 进程将磁盘上新的程序加载到内存后,会和该进程的页表重新建立映射关系。
2、替换原理
- 用fork创建子进程后,子进程可以调用exec系列函数的一种以执行另一个程序。
- 当进程调用exec系列函数的一种时,该进程用户空间中的代码和数据完全被新程序替换,从新程序的启动例程开始执行。
- 调用exec系列函数并不会创建新的进程,所以进程在调用exec系列函数前后,该进程的id并不会改变。
3、exec系列函数
- 这些函数如果调用成功则加载新的程序,从启动代码开始执行,不会有返回值。
- 如果调用出错则返回-1,所以,exec系列函数只有调用出错时才会有返回值。
- 第二张的execve函数是系统调用。即第一张图片的六个函数最终都会调用execve函数。
4、代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/wait.h>
5 #include<stdlib.h>
6
7 #define NUM 32
8 #define SIZE 16
9
10 int main()
11 {
12 char* _argv[NUM] = {
13 (char*)"ls",
14 (char*)"-l",
15 (char*)"-a",
16 NULL
17 };
18 char* const _envp[SIZE] = { //_envp的元素是char*类型的,_envp可以加const修饰,也可以不加
19 (char*) "snowdragon=writed 666", //字符串=前面不要加空格,否则它的名字就变了,即需要加空格才是变量名
20 NULL
21 };
22 pid_t id = fork();
23 if(id == 0)
24 {
25 //子进程
26 printf("子进程,pid:%d\n", getpid());
27 sleep(1);
28
29 //_envp是用来存储环境变量的,而不是存储选项的。选项不要加-
30 execle("./mycmd", "./mycmd", "a", NULL, _envp);
31 //execlp("ls", "ls", "-l", "-a", NULL);
32 //execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
33 //execv("/usr/bin/ls", _argv);
34 //execvp("ls", _argv);
35 exit(1);
36 }
37 else
38 {
39 printf("父进程,pid:%d\n", getpid());
40 int status = 0;
41 pid_t res = waitpid(id, &status, 0);
42 if(res > 0)
43 {
44 printf("等待子进程成功,退出码:%d\n", WEXITSTATUS(status));
45 }
46 }
47 return 0;
48 }
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4
5 int main(int argc, char* argv[]) //argv是指针数组
6 {
7 if(argc != 2)
8 {
9 printf("%d\n", argc);
10 printf("程序执行失败!\n");
11 exit(1);
12 }
13
14 printf("snowdragon:%s\n", getenv("snowdragon")); //环境变量名需要与传入的环境变量名相同才能获取到
15
16 if(strcmp(argv[1], "a") == 0) //需要用strcmp对两个值进行判断,而不能用==直接判断
17 {
18 printf("snowdragon receive a!\n");
19 }
20 else if(strcmp(argv[1], "b") == 0)
21 {
22 printf("snowdragon receive b!\n");
23 }
24 else if(strcmp(argv[1], "c") == 0)
25 {
26 printf("snowdragon receive c!\n");
27 }
28 else
29 {
30 printf("default\n");
31 }
32
33 return 0;
34 }
5、运行结果
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
已为社区贡献1条内容
所有评论(0)