操作系统实践04—进程管理
文章目录
1.创建进程
1.1 进程的定义
定义如下:
-
应用程序关于某数据集合上的一次运行活动;
-
操作系统进行资源分配和调度的基本单位。
进程是程序的一次执行过程,进程是动态的,程序是静态的,同一程序同时运行于若干个数据集合上,该程序将对应与若干个不同的进程。
每个进程拥有独立的地址空间,地址空间包括代码段
、数据段
和堆栈段
:
-
代码段,存储程序的代码;
-
数据段,存储程序的全局变量和动态分配的内存;
-
堆栈段,存储函数运行时的局部变量。
进程之间的地址空间是隔离的:
-
一个进程崩溃不会影响另一个进程;
-
一个进程崩溃不会影响到操作系统。
1.2 进程的属性
进程控制块:操作系统使用一个结构体记录进程的各类属性,该结构体被称为进程控制块。
进程标识有进程id和父进程id。
-
进程id,每个进程的id都是唯一的;
-
父进程id。
地址空间:
-
代码段的起始地址和长度;
-
数据段的起始地址和长度;
-
堆栈段的起始地址和长度。
打开文件列表:
-
打开文件时,在打开文件列表中记录被打开文件的信息;
-
关闭文件时,在打开文件列表中删除被关闭文件的信息;
-
进程终止时,操作系统遍历打开文件列表,将尚未关闭的文件关闭。
1.3 getpid原型
// 原型
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t
是C语言中用户自定义类型,在sys/types.h
中定义,typedef int pid_t;
。
-
getpid获取当前进程ID;
-
getppid获取父进程ID。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
printf("pid = %x, ppid = %x\n", getpid(), getppid());
return 0;
}
// pid = 18, ppid = 8
1.4 fork原型
// 原型
#include <unistd.h>
pid_t fork(void);
功能描述如下:
-
创建一个子进程,父子进程并发运行。
-
子进程复制父进程的如下属性:代码段、数据段的内容,父子进程拥有相同的代码和数据;打开文件列表。
-
不复制进程的PID属性,父子的进程的PID是唯一的。
返回值:
-
pid
是进程ID的缩写,pid_t
是使用typedef定义的进程ID类型。 -
父进程从fork返回处继续执行,在父进程中,fork返回子进程PID。
-
子进程从fork返回处开始执行,在子进程中,fork返回0。
例子1:
#include <stdio.h>
// 需要引入unistd.h,该头文件包含有fork函数声明
#include <unistd.h>
int main()
{
pid_t pid;
// 使用fork创建一个子进程
pid = fork();
// 打印fork的返回值
printf("pid = %d\n", pid);
// 根据pid是否为0,判断当前进程是子进程还是父进程
if (pid == 0)
// 在子进程中,fork返回0
printf("In child process\n");
else
// 在父进程中,fork返回子进程的pid
printf("In parent process\n");
return 0;
}
# 编译运行上面的程序,输入结果如下。
$ cc fork.c
$ ./a.out
pid = 32
In parent process
pid = 0
In child process
为什么if语句的then分支和else分支都会被执行?
每个进程都有一个独立的地址空间,当父进程执行fork函数时,会创建一个新的子进程,子进程也有一个独立的地址空间。
操作系统使用fork创建子进程的地址空间,把父进程地址空间的代码复制到子进程地址空间中,子进程的地址空间与父进程的地址空间的代码相同。
父进程执行fork()后,返回子进程的PID: 5327,父进程执行print语句,打印子进程的PID。
在父进程中,pid的值不为0,因此执行else分支。
子进程被创建后,从fork返回处开始执行,在子进程中,fork返回值为0,执行print语句,打印pid值。在子进程中,pid的值为0,因此执行then分支。
这是由父子进程并发运行导致的输出结果。
例子2:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == 0)
printf("In child: child PID = %d, parent PID = %d\n", getpid(), getppid());
else
printf("In parent: parent PID = %d, child PID = %d\n", getpid(), pid);
return 0;
}
$ cc fork.c
$ ./a.out
In parent: parent PID = 39, child PID = 40
In child: child PID = 40, parent PID = 39
2.进程特性
2.1 并发特性
父进程和子进程并发运行:
-
父进程创建子进程后,父子进程都处于运行状态中;
-
两个进程的输出结果是交织在一起的。
两者的代码段内容相同:
-
父进程从fork()返回处执行,fork()返回为子进程的PID;
-
子进程从fork()返回处执行,fork()返回0。
例子1:
#include <stdio.h>
#include <unistd.h>
void child()
{
int i;
for (i = 0; i < 3; i++)
puts("child");
}
}
void parent()
{
int i;
for (i = 0; i < 3; i++)
puts("parent");
}
}
int main()
{
pid_t pid;
pid = fork();
if (pid == 0)
child();
else
parent();
return 0;
}
$ cc concurr.c
$ ./a.out
parent
parent
parent
child
child
child
从输出结果看,父子进程似乎是串行执行的,父进程先输出3行字符串,然后子进程再输出3行字符串。
例子2:
#include <stdio.h>
#include <unistd.h>
void child()
{
int i;
for (i = 0; i < 3; i++)
puts("child");
// sleep函数,每输出一行字符串,暂停1秒
sleep(1);
}
}
void parent()
{
int i;
for (i = 0; i < 3; i++)
puts("parent");
// sleep函数,每输出一行字符串,暂停1秒
sleep(1);
}
}
int main()
{
pid_t pid;
pid = fork();
if (pid == 0)
child();
else
parent();
return 0;
}
$ cc concurr.c
$ ./a.out
parent
child
parent
child
parent
child
从输出结果看,父子进程的输出是交织在一起的,故父子进程是并发执行的。
2.2 fork的实现细节
操作系统为子进程创建PCB(进程控制块):
-
把父进程的大部分属性复制到子进程的PCB中;
-
不复制PID属性,父子进程拥有不同的PID。
操作系统为子进程创建地址空间:
- 把父进程的代码和数据复制到子进程的地址空间中。
2.3 隔离特性
进程的地址空间是互相隔离的:
-
每个进程拥有自己的地址空间;
-
进程仅能访问自己的地址空间;
-
如果出现非法内存访问,仅仅当前进程受到影响。
全局变量:
-
全局变量存在于两个地址空间中,并非被两个进程共享;
-
父进程和子进程访问的是自己的全局变量,互相不影响。
例子1:
#include <stdio.h>
#include <unistd.h>
int global = 0;
void child()
{
for (int i = 0; i < 3; i++) {
global++;
printf("In child, global = %d\n", global);
sleep(1);
}
}
void parent()
{
for (int i = 0; i < 3; i++) {
global++;
printf("In parent, global = %d\n", global);
sleep(1);
}
}
int main()
{
child();
parent();
return 0;
}
$ cc isolate.c
$ ./a.out
In child, global = 1
In child, global = 2
In child, global = 3
In parent, global = 4
In parent, global = 5
In parent, global = 6
可以看出程序是串行执行的。
例子2:
#include <stdio.h>
#include <unistd.h>
int global = 0;
void child()
{
for (int i = 0; i < 3; i++) {
global++;
printf("In child, global = %d\n", global);
sleep(1);
}
}
void parent()
{
for (int i = 0; i < 3; i++) {
global++;
printf("In parent, global = %d\n", global);
sleep(1);
}
}
int main()
{
pid_t pid;
pid = fork();
if (pid == 0)
child();
else
parent();
return 0;
}
$ cc isolate.c
$ ./a.out
In parent, global = 1
In child, global = 1
In parent, global = 2
In child, global = 2
In parent, global = 3
In child, global = 3
global变量存在于两个地址空间中,父进程和子进程访问的是自己的global变量,互相不影响。
3.装入程序
3.1 命令行参数
C程序的main函数原型如下:
int main(int argc, char *argv[]);
操作系统将命令行参数传递给main函数,其中:
-
argc,命令行参数的个数;
-
argv,命令行参数数组。
# 简单例子如下
$ cp /etc/passwd passwd.bak
# argc = 3
# argv = {"cp", "/etc/passwd", "passwd.bak"}
命令名也被当作是命令参数,所以argc是3而不是2。
例子1:
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("argc = %d\n", argc);
for (i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}
$ cc cmd.c
$ ./a.out hello
argc = 2
argv[0] = ./a.out
argv[1] = hello
$ ./a.out a b c
argc = 4
argv[0] = ./a.out
argv[1] = a
argv[2] = b
argv[3] = c
3.2 绝对路径和命令名
装入程序的两种方式为:
- 可以输入命令的绝对路径调用程序;
- 也可以输入命令的命令名调用程序。
# which命令列出一条命令的绝对路径
$ which echo
/bin/echo
# 输入echo命令的绝对路径
$ /bin/echo a b c
a b c
# 输入echo命令的命令名
$ echo $PATH
/bin:/usr/bin:/sbin:/usr/sbin
$ echo a b c
a b c
3.3 execlp
3.3.1 execl原型
// 原型
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
功能:
-
将当前进程的地址空间的内容全部清空;
-
将path指定的可执行程序的代码和数据装入到当前进程的地址空间。
参数:
-
该函数的参数个数可变
-
最后一个参数必须是NULL
-
第一个参数path指定被装入程序的路径,可以是命令的绝对路径,也可以是命令的相对路径。
返回值:
-
装入失败后,返回值为-1;
-
装入成功后,从被装入程序的main函数开始执行。
例子1:
#include <stdio.h>
// 函数execl声明在文件unistd.h中
#include <unistd.h>
int main()
{
puts("before exec");
// 可执行程序的路径是/bin/echo
// 将4个参数echo, a, b, c传递给被装入程序echo
// 最后一个参数必须是NULL
int error = execl("/bin/echo", "echo", "a", "b", "c", NULL);
if (error < 0)
perror("execl");
puts("after exec");
return 0;
}
$ cc execl.c
$ ./a.out
before exec
a b c
为什么没有执行第二条puts语句?
执行execl系统调用时,会清空当前地址空间所有内容,并将/bin/echo程序的代码(如下)和数据装入到当前地址空间中。从echo程序的main函数开始执行,打印从execl系统调用传递过来的参数。当echo程序执行return返回后,进程就结束了。
/* /bin/echo程序的代码 */
#include <stdio.h>
int main(int argc, char *argv)
{
int i;
for (i = 1; i < argc; i++)
printf("%s ", argv[i]);
return 0;
}
3.3.2 execlp原型
// 原型
#include <unistd.h>
int execlp(const char *file, const char *arg, ...);
在功能上,与execl
相同。
execl和execlp的区别:在execl中,第一个参数指定可执行程序的路径。该路径可以是绝对路径,也可以是相对于当前工作目录的相对路径。
在execlp中,第一个参数指定可执行程序的路径:
-
该路径可以是绝对路径;
-
该路径可以是相对于当前工作目录的相对路径;
-
该路径可以是PATH环境变量指定目录下的相对路径。
#include <stdio.h>
#include <unistd.h>
int main()
{
puts("before exec");
// 使用execl
int error = execl("echo", "echo", "a", "b", "c", NULL);
// 使用execlp
int error = execlp("echo", "echo", "a", "b", "c", NULL);
if (error < 0)
perror("execl");
puts("after exec");
return 0;
}
# 使用excel
$ cc execlp.c
$ ./a.out
before exec
exec: No such file or directory
after exec
# 使用execlp
$ cc execlp.c
$ ./a.out
before exec
a b c
execl在装入程序时,不会在PATH环境变量指定的目录中查找。由于在当前目录下找不到程序echo,因此程序执行execl失败。
execlp搜索PATH环境变量指定的目录,在PATH环境指定的目录下找到程序echo,故执行成功。
3.4 execvp
3.4.1 execv原型
// 原型
#include <unistd.h>
int execv(const char *path, const char *argv[]);
在功能上,与execl
相同。argv
指定传递给程序的参数,argv
数组的最后一项必须是NULL指针。
execl和execv的区别:
- 函数execl末尾的
l
表示list,参数以列表的形式传递给可执行程序。
execl("/bin/echo", "echo", "a", "b", "c", NULL);
- 函数execv末尾的
v
表示vector,参数以数组的形式传递给可执行程序。
char *argv[] = {"echo", "a", "b", "c", NULL};
execv("/bin/echo", argv);
例子1:
#include <stdio.h>
#include <unistd.h>
int main()
{
puts("before exec");
// 参数存放在数组argv中,数组的最后一项必须是NULL指针
char *argv[] = {"echo", "a", "b", "c", NULL};
int error = execv("/bin/echo", argv);
if (error < 0)
perror("exec");
puts("after exec");
return 0;
}
$ cc execv.c
$ ./a.out
before exec
a b c
3.4.2 execvp原型
// 原型
#include <unistd.h>
int execvp(const char *file, char *argv[]);
在功能上,与execv
相同。
execv和execvp的区别:在execv中,第一个参数指定可执行程序的路径,该路径可以是绝对路径,也可以是相对于当前工作目录的相对路径。
在execvp中,第一个参数指定可执行程序的路径:
-
该路径可以是绝对路径;
-
该路径可以是相对于当前工作目录的相对路径;
-
该路径可以是PATH环境变量指定目录下的相对路径。
例子1:
#include <stdio.h>
#include <unistd.h>
int main()
{
puts("before exec");
char *argv[] = {"echo", "a", "b", "c", NULL};
// 使用execv
int error = execv("echo", argv);
// 使用execvp
int error = execvp("echo", argv);
if (error < 0)
perror("execv");
puts("after exec");
return 0;
}
# 使用execv
$ cc execvp.c
$ ./a.out
before exec
exec: No such file or directory
after exec
# 使用execvp
$ cc execvp.c
$ ./a.out
before exec
a b c
4.退出进程
4.1 exit原型
// 原型
#include <stdlib.h>
void exit(int status);
该函数功能是正常退出当前进程,并将status & 0xFF
作为退出码返回给父进程。
预定义常量:
-
EXIT_SUCCESS,为0的数值,表示程序正常退出。
-
EXIT_FAILURE,为非0的数值,表示程序执行过程发生了错误,异常退出。
在linux shell中,可以通过特殊的环境变量$?
获得程序的退出码。
例子1:
#include <stdio.h>
#include <stdlib.h>
int main()
{
puts("before exit");
exit(100);
puts("after exit");
return 0;
}
$ cc exit.c
$ ./a.out
before exit
# 获得程序的退出码
$ echo $?
100
例子2:
#include <stdio.h>
int main()
{
puts("before return");
// main函数返回100,该值即为退出码
return 100;
}
$ cc return.c
$ ./a.out
before return
$ echo $?
100
4.2 atexit原型
// 原型
#include <stdlib.h>
int atexit(void (*function)(void));
功能:
-
注册一个回调函数function,进程正常结束时,function会被调用;
-
如果注册多个回调函数,进程结束时,以与注册相反的顺序调用回调函数。
例子1:
#include <stdio.h>
#include <stdlib.h>
void f1()
{
puts("f1");
}
void f2()
{
puts("f2");
}
void f3()
{
puts("f3");
}
int main()
{
// 使用atexit注册3个函数,首先是f1,然后是f2,最后是f3
// 程序退出时,以相反的顺序调用这3个函数
atexit(f1);
atexit(f2);
atexit(f3);
puts("main");
return 0;
}
$ cc atexit.c
$ ./a.out
main
f3
f2
f1
使用atexit
函数,可以理解为将多个回调函数压入栈中,当进程结束后,则将回调函数出栈。
5.等待进程
5.1 wait原型
// 原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能是等待子程序结束。参数status
如果不为NULL,子进程的退出码保存在status指向的变量中。
5.2 退出码
进程可能由于不同的原因退出:
-
主动调用exit正常退出;
-
接受信号后退出。
查询退出原因的宏:
名称 | 功能 |
---|---|
WIFEXITED(status) | 如果进程通过调用exit正常退出,则返回真 |
WEXITSTATUS(status) | 如果进程通过调用exit正常退出,返回进程的退出码 |
WIFSIGNALED(status) | 如果进程接受信号后退出,则返回真 |
WTERMSIG(status) | 如果进程接受信号后退出,返回导致进程退出的信号 |
5.3 实践
例子1:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void child()
{
puts("Child process");
exit(123);
}
int main()
{
int pid;
pid = fork();
if (pid == 0)
child();
wait(NULL);
puts("Parent process");
return 0;
}
$ cc wait1.c
$ ./a.out
Child process
Parent process
父进程执行wait函数,wait的参数为NULL,表示忽略子进程的返回码。等待子进程结束后,父进程打印字符串。
例子2:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void child()
{
// 子进程以123作为退出码调用exit
exit(123);
}
int main()
{
int pid;
pid = fork();
if (pid == 0)
child();
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("WIFEXITED = true\n");
printf("WEXITSTATUS = %d\n", WEXITSTATUS(status));
}
return 0;
}
$ cc wait2.c
$ ./a.out
WIFEXITED = true
WEXITSTATUS = 123
父进程调用wait,将变量status的地址传递给wait,等待子进程结束。变量status用于接受子进程的退出码。WIFEXITED
判断子进程是否是通过调用exit退出的;WEXITSTATUS
打印子进程的退出码。