操作系统实践04—进程管理

操作系统实践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打印子进程的退出码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暄踽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值