目录
程序创建进程
程序:指令和数据的集合,一般作为目标文件保存在磁盘中。
进程:程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
可执行程序位于磁盘中,需要将静态程序加载到内存生成动态的进程,CPU才可以不停地取指执行。程序在硬盘中主要包含代码(code)及静态数据(static data),而相比之下,进程似乎多了堆(heap)和(stack)。不止如此,每个进程还拥有自己的身份证——进程控制块。这些是怎么来的呢?下面将从虚拟地址空间开始逐一阐明。
1. 虚拟地址空间
虚拟内存:内存管理的一种方式, 磁盘上划分出一块空间由操作系统管理,物理内存耗尽时可以充当物理内存来使用。虚拟内存可以通过页表(page table)映射到物理内存。
虚拟地址空间:在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中,这个沙盘就是虚拟地址空间(virtual address space)。虚拟地址空间由内核空间(kernel space)和用户空间(user space)两部分组成。
2.进程控制块
操作系统在创建进程时会在内核空间配备一个进程控制块(PCB),
包含描述进程当前情况及管理进程全部信息的数据结构。在Linux操作系统中的进程控制块实际是一个task_struct结构体,放在sched.h,以下简要介绍。
(1) 进程状态(process state):进程的常见运行状态包括就绪(Ready)、阻塞(Blocked)及运行(Running)。
enum proc_state { READY, RUNNING, READY };
就绪(Ready):进程拥有运行所需的所有资源,等待分配CPU。
运行(Running):进程运行在CPU上,即CPU正在运行该进程包含的指令。
阻塞(Blocked)/等待(Waiting):进程在运行时发生CPU以外事件的请求(如I/O请求)从而放弃CPU使用权。
Simulation Homework: 模拟进程状态转换
(2) 进程标识符(process identifier/number):描述本进程的唯一标识符,用来区别其他进程。
int pid; // Process ID
(3) 程序计数器(program counter)及寄存器信息(registers):进程切换时的入口地址及需要保存的信息
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip; // 程序计数器(PC),存放下一个CPU指令存放的内存地址
int ebx; // 基址寄存器, 在内存寻址时存放基地址
int ecx; // 计数器(counter),loop循环的内定计数器
int edx; // 用来放整数除法产生的余数
int esi; // 源变址寄存器
int edi; // 目的变址寄存器
int esp; // 栈指针寄存器
int ebp; // 基址指针寄存器
};
(4)内存限制(memory limits)
char *mem; // Start of process memory
uint sz; // Size of process memory
(5)打开文件列表
struct file *ofile[NOFILE]; // Open files
(6) 进程指针(pointer):采用指针将进程控制块之间相互链接
进程链表组织方式是系统设置就绪队列头指针、阻塞队列头指针、运行队列头指针,按照进程的状态将进程的PCB挂在对应头指针后组成队列
3.用户空间分区
源程序包含代码及数据,编译链接生成的可执行文件时汇编形式,装载到内存中是二进制文件。程序是通过变量访问数据,但在二进制下是没有变量的概念,只能通过内存地址访问数据。如果全部变量都通过地址访问,这样是低效且不现实的。因而,需要根据不同变量的性质进行分区,比如局部变量内容短小,需要频繁访问,但是生命周期很短,通常只在一个方法内存活,就专门从内存划分出一块较小区域命名为栈(stack),由编译器分配与回收,效率高。又比如较大的结构体可能不需要太频繁的访问,但生命周期较长,通常很多个方法中都会用到,就划出另一较大区域命名为堆(heap),由程序员自主分配回收。
4.创建进程
创建进程时,操作系统为进程生成唯一的进程控制块(PCB)并挂在进程队列,内存中开辟空间存放程序的变量及代码,栈内装入输入的参数argc/argv
,清空寄存器内容,main()
入口地址送到程序计数器PC;CPU执行程序main()
的指令,遇到return
语句返回操作系统;操作系统释放进程内容,并将进程从进程队列中移除。
说清楚程序是如何变成进程的,下面说说如何进程中操作进程—API。
进程创建进程—接口API/C库函数
1. 创建子进程fork()
(1) 头文件
#include <unistd.h>
(2) 函数原型
#define int pid_t
pid_t fork( void);
返回值:成功调用一次则返回两个值,父进程返回子进程PID,子进程返回0;调用失败则返回-1。
(3) 函数说明
主函数main()运行时会自动创建进程,称为父进程;fork()系统调用用于创建一个新进程,称为子进程。创建子进程时,子进程会在内核中拥有自己的进程控制块(task_struct),从而拥有不同于父进程的PID。但同时,子进程复制父进程其余一切(堆栈、代码段等),而fork()作为系统调用保存在父进程和子进程的栈中,因而会返回两次,产生两个返回值。
// p1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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) { // 子进程内rc=0
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // 父进程内rc为子进程ID getpid()为父进程ID
printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
}
return 0;
}
父进程PID为3838,子进程PID为3829。创建子进程后,两个进程将执行fork()系统调用之后的下一条指令,因而不会再输出hello world
,但可以根据rc不同返回值输出下面两行内容。
2. 阻塞当前进程wait()
(1) 头文件
#include <sys/wait.h>
(2) 函数原型
#define int pid_t
pid_t wait (int * status);
参数:status 不是NULL时,子进程的结束状态值会由参数 status 返回;如果不关心子进程结束状态可置status为NULL。
返回值:执行成功则返回子进程PID,失败则返回-1。
(3) 函数说明
不使用wait()时,父进程和子进程会同时运行;使用wait()后,父进程会等待子进程执行完毕并返回子进程PID后再执行。
// p2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
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) { // 子进程内rc=0
printf("hello, I am child (pid:%d)\n", (int) getpid());
sleep(1); // 等待一段时间再退出当前进程
} else { // wc为子进程PID
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
父进程PID为838,子进程PID为841,wait()返回值wc=841。
3. exec()函数簇
(1) 头文件
#include <unistd.h>
(2) 函数原型
exec指的是一组函数族,并不存在某一个具体的exec(),现选取execvp()作为例子了解。
int execvp(const char *file, char *const argv[]);
参数:file为需要运行的文件名,argv[]为输入的参数列表
返回值:执行成功则函数不会返回,执行失败则直接返回-1
(3) 函数说明
exec()函数簇可以使子进程摆脱和父进程内容的相似性,执行一个完全不同的程序。
// p3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
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]; // strdup()字符串拷贝库函数
myargs[0] = strdup("wc"); // 程序: "wc" (字符统计)
myargs[1] = strdup("p3.c"); // 参数: 需要统计的文件
myargs[2] = NULL; // 命令行结束标志
execvp(myargs[0], myargs); // 统计行、单词、字节数
printf("this shouldn't print out");
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int) getpid());
}
return 0;
}
子进程重载字符统计程序,统计p3.c的行数为32
、单词数为123
、字节数为966
。
4. 接口API有什么用?
在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器),诸如windows下的cmd、unix下的bash。打开shell时,shell相当于父进程,shell可以接受命令,然后利用fork()创建子进程执行命令,执行完毕结束子进程返回shell,从而接受下一条命令。
(1) 控制台输入wc p3.c > newfile.rtf
,则会在后台创建子进程统计p3.c
的行数、单词数及字节数,并写入newfile.rtf
。
(2) 控制台输入./p4
,则会在后台创建子进程统计p4.c
的行数、单词数及字节数,然后创建p4.output
并写入,输入cat p4.output
可显示内容。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
// 重定向输出到文件
close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
// 重载"wc"程序
char *myargs[3]; // strdup()字符串拷贝库函数
myargs[0] = strdup("wc"); // 程序: "wc" (字符统计)
myargs[1] = strdup("p4.c"); // 参数: 需要统计的文件
myargs[2] = NULL; // 命令行结束标志
execvp(myargs[0], myargs); // 统计行、单词、字节数
} else {
int wc = wait(NULL);
assert(wc >= 0);
}
return 0;
}
5. 系统命令、接口API和系统调用关系
系统调用(System call):操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。例如,用户可以通过进程系统调用sys_fork()创建进程。
应用程序接口API(Application Programming Interface):程序员在用户空间下可以直接使用的函数接口,是一些预定义的函数,比如fork()函数,提供应用程序访问一组系统调用的能力。
系统命令:系统命令相对于API更高了一层,它实际上是一个可执行程序,它的内部引用了用户编程接口(API)来实现相应的功能
过程如下:使用系统命令gcc -o p1 p1.c -Wall -Werror
调用gcc编译器编译生成可执行程序p1
,然后使用系统命令./p1
执行该程序创建进程,进程内运行API函数fork()
,根据frok()
的系统调用号寻找内核空间对应的系统调用的sys_fork()
创建子进程。