Q1:什么是进程?
进程就是运行中的程序。程序是存在磁盘上的一些指令的合集,包含一些系统调用、过程调用以及静态数据等。程序是没有生命周期的,谁听说过某某某程序到了晚上8点就不执行了。而进程是有生命周期的。
Q2:为什么需要进程/为什么需要虚拟化?
日常使用计算机的时候,可能同时会处理多种事情,比如说一边跑着深度学习框架,一边看视频、玩LOL。系统中存在多个进程同时进行,这时候就需要虚拟化
virtualization
,让CPU提供这样一种假象,一个进程只运行一个时间片,然后就切换到其他进程。进程是操作系统提供的抽象。
Q3:程序如何转换成进程的?
OS 运行必须做的一件事就是把代码和所有的静态数据加载到内存中,加载到进程的地址空间中。C语言程序使用栈存放局部变量、函数参数和返回地址。操作系统分配这些内存,并提供给进程。 操作系统也可能会用参数初始化栈。对于动态开辟的空间比如说malloc、realloc以及calloc开辟的空间则是存放在堆区,跟链表、树、散列表等一样。同样的操作系统也会分配相当的内存给进程。
1、fork()
系统调用,创建进程,示例代码
#include
#include
#include
int main(int argc, char **argv) {
printf("hello world (pid : %d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("hello, I am child (pid : %d)\n", (int)getpid());
} else {
printf("hello, I am parent of %d (pid : %d)\n", rc, (int)getpid());
}
return 0;
}
2、wait()
子进程与父进程输出顺序可能不一定,但是可以使用wait(NULL)
来确定要delay的父/子进程
#include
#include
#include
#include
int main() {
printf("hello world (pid : %d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("hello, I am child (pid : %d)\n", (int)getpid());
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc : %d) (pid : %d)\n", rc, wc, (int)getpid());
}
return 0;
}
3、exec族函数
系统调用,可以让各个进程执行不同的任务。
//通过man手册查询到的exec家族函数及参数列表如下
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[]);
#include
#include
#include
#include
#include
int main(int argc, char **argv) {
printf("hello world. (pid : %d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("hello, I am child (pid : %d)\n", (int)getpid());
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("p3.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);
printf("This should not print out!\n");
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc : %d) (pid : %d)\n", rc, wc, (int)getpid());
}
return 0;
}
Q : 过程调用与系统调用区别?
过程调用,简言之就是在一个程序中调用另一个程序,或者说另一段程序。系统调用就是在用户程序中调用系统提供的功能子程序,说白了就是程序员用的API。过程调用更多的是静态调用,调用程序和被调用程序往往在一个程序中。并且过程函数在编译之后对应的逻辑地址是不变的。系统调用是一种动态调用,系统调用的处理代码独立于程序之外。
**注意:**系统调用也是一种过程调用,但是是一种特殊的过程调用,操作系统使用陷入指令隐藏了这部分系统调用内部实现。
为了让程序执行受限制的操作,操作系统把执行过程分成内核态和用户态。目的是使得进程既能够执行I/O请求和其他操作又不至于完全获得操作系统的控制权。
**用户态:**应用程序不能完全访问硬件资源
**内核态:**操作系统可以访问全部资源,包括硬件资源。
为了使得系统在用户态与内核态的切换,操作系统提供陷入指令。**陷入指令:**负责用户态到内核态,以及内核态到用户态的切换。
内核态 | 硬件 | 用户态 |
---|---|---|
在进程列表上创建条目 为程序分配内存 将程序加载到内存中 根据 argv 设置程序栈 用寄存器/程序计数器填充内核栈 从陷阱返回 |
||
从内核栈恢复寄存器 转向用户模式 跳到 main |
||
运行 main …… 调用系统调用 陷入操作系统 |
||
将寄存器保存到内核栈 转向内核模式 跳到陷阱处理程序 |
||
处理陷阱 做系统调用的工作 从陷阱返回 |
||
从内核栈恢复寄存器 转向用户模式 跳到陷阱之后的程序计数器 |
||
……从 main 返回 陷入(通过 exit()) |
||
释放进程的内存将进程 从进程列表中清除 |
Q:不同进程之间是如何切换的呢?
主要有以下几种方式
1、协作方式:等待系统调用。运行时间过长的进程被假定会定期放弃 CPU
2、非协作方式:操作系统控制,利用时钟中断,就是让正在执行的进程停止,赋予另外一个进程操作系统的部分控制权,然后再进行切换。也是最主要的手段,因此在操作系统启动的时候,时钟中断也会启动。3、保存和恢复上下文:操作系统要做的就是为当前正在执行的进程保存一些寄存器 的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。 这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程, 而是继续执行操一个进程。所谓的上下文切换,就是程序状态的加载和程序状态 的保存。
时钟中断执行流程如下,以两个进程A、B为例:
内核态 | 硬件 | 用户态 |
---|---|---|
进程 A…… | ||
时钟中断 将寄存器(A)保存到内核栈(A) 转向内核模式 跳到陷阱处理程序 |
||
处理陷阱 调用 switch()例程 将寄存器(A)保存到进程结构(A) 将进程结构(B)恢复到寄存器(B) 从陷阱返回(进入 B) |
||
从内核栈(B)恢复寄存器(B) 转向用户模式 跳到 B 的程序计数器 |
||
进程 B…… |
在此协议中,有两种类型的寄存器保存/恢复:
第一种是发生时钟中断的时候。 在这种情况下,运行进程的用户寄存器由硬件隐式保存,使用该进程的内核栈。
第二种是当操作系统决定从 A 切换到 B。在这种情况下,内核寄存器被软件(即 OS)明确地保存, 但这一被存储在该进程的进程结构的内存中。后一个操作让系统从好像刚刚由 A 陷入内核, 变成好像刚刚由 B 陷入内核。
//测量系统调用成本,执行0字节读取,记录时间
/**
int gettimeofday(struct timeval *tv, struct timezone *tz);这是获取当前时间系统调用,返回的是微妙
数据结构如下:
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
*/
#include
#include
#include
int main() {
int fd = open("./test", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
if (fd == -1) {
fprintf(stderr, "open file failed\n");
exit(1);
}
char buff[10];
struct timeval start, end;
for (int i = 0; i < 10; i++) {
gettimeofday(&start, NULL);
read(fd, buff, 0);
gettimeofday(&end, NULL);
printf("Use : %ld\n", end.tv_usec - start.tv_usec);
printf("%ld : %ld\n", end.tv_usec, start.tv_usec);
}
close(fd);
return 0;
}
//上下文切换,第一个进程向第一个管道写入数据,然后等待第二个数据的读取。第二个管道写入,第一个管道读取,计算上下文切换时间差。
#include
#include
#include
#include
int main() {
int pi1[2], pi2[2];
char buff1[30], buff2[30];
int p1 = pipe(pi1);
int p2 = pipe(pi2);
if (p1 < 0 || p2 < 0) {
fprintf(stderr, "pipe failed\n");
exit(1);
}
struct timeval start, end;
int rc = fork();
for (int i = 0; i < 10; i++) {
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
read(pi1[0], buff1, 25);
gettimeofday(&end, NULL);
printf("child : %ld\n", end.tv_usec - atol(buff1));
gettimeofday(&start, NULL);
sprintf(buff2, "%ld", start.tv_usec);
write(pi2[1], buff2, 25);
} else {
gettimeofday(&start, NULL);
sprintf(buff1, "%ld", start.tv_usec);
write(pi1[1], buff1, 25);
read(pi2[0], buff2, 25);
gettimeofday(&end, NULL);
printf("father : %ld\n", end.tv_usec - atol(buff2));
}
}
return 0;
}