Skip to content

[!quote] 操作系统

  • 操作系统是虚拟机:操作系统将物理资源【处理器,内存,磁盘】转换为更通用,更强大,更易于使用的虚拟形式
  • 操作系统是标准库:操作系统提供了 API 来供使用者调用,以此来运行程序,访问内存和设备,并进行其他相关操作
  • 操作系统是资源管理器:操作系统虚拟化让许多程序并发运行【从而共享 CPU】,让许多程序可以同时访问自己的指令和数据【从而共享内存】,让许多程序访问设备【从而共享磁盘

进程

[!quote] 进程 进程 就是程序的一次独立运行。一个进程在执行过程中会访问和影响计算机系统的不同部分【内存、CPU、磁盘 ……】,这些部分的状态和内容会随着进程的执行而改变,而这些变化可以被记录下来并概括为一个进程

启动进程之前,操作系统做了什么

  • 将代码和静态数据加载到内存中
  • 为程序的运行时的栈分配一些内存,需要时也会为程序的堆分配内存
  • 执行与 IO 设置相关的其他工作

进程的状态

![[Excalidraw/计算机/四大件/操作系统 Draw.md#^group=Rtlv2f-IRxbCmA8eWSrGi|385]]

  • 初始:进程在创建时处于的状态
  • 运行:进程正在处理器上运行,这意味着它正在执行指令
  • 就绪:进程已准备好运行,但由于某种原因,操作系统选择不在此时运行
  • 阻塞:进程执行某种操作时,需要等待其他事件的发生,才会准备运行
  • 最终:进程处于已退出但尚未清理的状态

进程 API

fork() 系统调用

c
int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {                   // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {               // child (new process)
        printf("hello, I am child (pid:%d)\n", (int)getpid());
    } else {                           // parent goes down this path (main)
        printf("hello, I am parent of %d (pid:%d)\n",rc, (int)getpid());
    }
    return 0;
}


hello world (pid:29146)                  //pid是进程描述符
hello, I am parent of 29147 (pid:29146)        //在这里父进程先运行完
hello, I am child (pid:29147)

当一个进程调用 fork() 系统调用时,操作系统会复制该进程的执行环境(包括内存空间、寄存器状态、上下文等),并为其创建一个新的进程。这个新进程和原进程几乎完全一样,它们的代码、数据和堆栈等都是相同的。 此外,子进程和父进程还有一些细微的差别【例如子进程中的某些变量可能与父进程不同,这是因为子进程和父进程是独立的进程,它们各自都有自己的内存空间和数据结构】

在这个过程中,子进程不会从 main() 函数开始执行,而是从 fork() 系统调用之后的代码开始执行。这是因为子进程会复制父进程的执行环境,包括程序计数器等信息,因此它会从 fork() 系统调用之后的代码开始执行。 因此,在原进程和子进程中,printf() 函数只会被调用一次,输出一条 "hello world" 的信息。

原进程和新进程都会从 fork() 系统调用中返回,子进程会返回 0,而父进程会返回子进程的进程 ID^1

wait() 系统调用

c
int main(int argc, char *argv[])
{
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {                   // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {                 // child (new process)
        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;
}


hello world (pid:29266)
hello, I am child (pid:29267)        
hello, I am parent of 29267 (wc:29267) (pid:29266)

这里其实是 有可能父进程先完成的【当父进程碰巧先运行,那么会马上调用 wait(),之后运行子进程,但是在子进程还未运行 printf() 时,wait() 返回了,接着父进程继续运行直到结束】

exec() 系统调用

c
int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0) {                      // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {                // child (new process)
        printf("hello, I am child (pid:%d)\n", (int)getpid());
        char *myargs[3];
        myargs[0] = strdup("wc");            // program: "wc" (word count)
        myargs[1] = strdup("p3.c");           // argument: file to count
        myargs[2] = NULL;                     // marks end of array
        execvp(myargs[0], myargs);            // 运行字符计数程序
        printf("this shouldn’t print out");
	}else {                       // parent goes down this path (main)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int)getpid());
    }
    return 0;
}

给定可执行程序的名称(如 wc)及需要的参数(如 p3.c)后,exec() 会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段,静态数据,堆、栈及其他内存空间,它们都会被重新初始化。 然后操作系统就执行该程序,将参数通过 argv 传递给该进程 因此,它并没有创建新进程,而是直接将当前运行的程序(以前的 p3)替换为不同的运行程序(wc) 子进程执行 exec() 之后,几乎就像 p3.c 从未运行过一样。对 exec() 的成功调用永远不会返回