操作系统笔记(1)-进程
操作系统笔记(1)-进程
概念
进程的非正式定义:
进程就是运行中的程序
程序如何转化为进程:
操作系统将代码和所有静态数据加载到内存中, 然后为程序的运行时栈分配一些内存, 也可能为程序的堆分配内存
. 然后启动程序,跳转到程序的入口, 即main()
函数
进程状态
进程在不同的时间可能处于不同的状态, 一般来说, 进程可以处于以下 3 种状态之一:
运行 : 在运行状态下, 进程正在处理器上运行. 这意味着它正在执行指令
就绪 : 在就绪状态下, 进程已准备好运行, 但由于某种原因, 操作系统选择不在此时运行 ( 一般是由于操作系统的调度)
阻塞 : 在阻塞状态下, 一个进程执行了某种操作, 直到发生其他事件时才会准备运行. 一个常见的例子是, 当进程向磁盘发起
I/O
请求时, 它会被阻塞, 因此其他进程可以使用处理器
注 : 除了运行、就绪和阻塞之外, 还有其他一些进程可以处于的状态. 有时系统会有一个 初始
状态, 表示进程在创建时处于的状态. 另外一个是 终止
状态, 表示进程处于已退出但尚未清理的状态, 比如说 僵尸进程
, 一般需要在父进程等待, 告诉操作系统清理这个进程的相关数据结构
进程 API
Linux 系统创建新进程的方式是通过一对系统调用
fork()
和exec()
进程还可以通过
wait()
或waitpid()
系统调用来等待其创建的子进程结束 (避免僵死进程产生)
fork
系统调用
接口声明:
#include <unistd.h>
pid_t fork(void);
理解
fork()
最困难之处在于调用它一次,它却返回两次。它在调用进程(称为父进程)中返回一次,返回值是新派生进程(称为子线程)的进程ID号,在子进程又返回一次,返回值为0。因此,返回值本身告知当前进程是子进程还是父进程fork()
在子进程返回0
而不是父进程的进程PID
的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid()
取得父进程的进程PID
。相反,父进程可以有许多子进程,而且无法获取各个子进程的进程 PID。如果父进程想要跟踪所有子进程的进程PID
,那么它必须记录每次调用fork()
的返回值父进程中调用 fork 之前打开的所有描述符在 fork 返回之后由子进程分享网络服务器利用了这个特性:父进程调用accept 之后调用 fork。所接受的已连接套接字随后就在父进程与子进程之间共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。
wait()
和 waitpid()
系统调用
处理已终止的子进程
#include <sys/wait.h>
// 均返回:若成功则为进程ID,若出错则为0或-1
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
函数
wait()
和waitpid()
均返回两个值:已终止子进程的进程PID
号,以及通过statloc
指针返回的子进程终止状态(一个整数)。我们可以调用三个宏来检查终止状态,并辨别子进程是正常终止、由某个信号杀死还是仅仅由作业控制停止。如果调用
wait()
的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait()
将阻塞到现有子进程第一个终止为止waitpid()
函数就等待哪个进程以及是否阻塞给了我们更多的控制。首先,pid
参数允许我们制定想等待的进程PID
, 值-1
表示等待第一个终止的子进程。其次,options
参数允许我们指定附加选项。最常用的选项是WNOHANG
,它告知内核在没有已终止子进程时不要阻塞。
API 实例
创建子进程, 然后使用 wait()
等待子进程结束, 打印各自的 PID
#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) { // fork 失败
fprintf(stderr, "fork failed");
exit(1);
}
else if (rc == 0) { // 子进程
printf("hello, I am child (pid:%d)\n", (int)getpid());
}
else { // 父进程
wait(NULL); // 等待子进程结束
printf("hello, I am parent of %d (pid:%d)\n", rc, (int)getpid());
}
return 0;
}
/*
output:
hello world (pid:5459)
hello, I am child (pid:5460)
hello, I am parent of 5460 (pid:5459)
*/
观察可知, 子进程不会从
main()
函数开始执行, 而是直接从fork()
系统调用返回, 就好像是它自己调用了fork()
子进程并不是完全拷贝了父进程, 虽然它拥有自己的地址空间、寄存器、程序计数器等等, 但是它从
fork()
的返回值的不同的, 上述例子由于使用了wait()
来等待因此结果是一致的, 但若不等待则顺序可能不同
exec()
系统调用
一个进程想要执行另外一个程序, 唯一方法是先调用 fork()
创建子进程, 然后在子进程调用 exec()
将当前运行的子进程替换为不同的程序. 实际上, exec()
:
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char*) 0 */);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathname, const char *arg0, ... /* (cahr *) 0, char *const envp[] */);
int execvpe(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *) 0 */);
int execvp(const char *filename, char *const argv[]);
依次传入可执行程序的
路径
和参数
, 就可以从程序中加载代码和静态数据, 并用它覆写自己的代码段 (以及静态数据), 堆、栈及其他内存空间也会被重新初始化这些函数只在出错时才返回到调用者。否则,控制将被传递给新程序的起始点, 通常就是
main()
函数
API 实例
创建子进程, 然后调用 execvp()
执行 wc
程序, 返回指定文件的行、单词和字节数
#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) { // fork 失败
fprintf(stderr, "fork failed");
exit(1);
}
else if (rc == 0) { // 子进程
printf("hello, I am child (pid:%d)\n", (int)getpid());
char* args[3];
args[0] = strdup("wc");
args[1] = strdup("p2.c");
args[2] = NULL; // 标志着参数的结尾
execvp(args[0], args);
// 下面的语句不会被打印出来, 因为控制被传递给新程序的起始点了
printf("this shouldn't print out");
}
else { // 父进程
wait(NULL); // 等待子进程结束
printf("hello, I am parent of %d (pid:%d)\n", rc, (int)getpid());
}
return 0;
}
/*
output:
hello world (pid:5512)
hello, I am child (pid:5513)
30 94 911 p2.c
hello, I am parent of 5513 (pid:5512)
*/
应用: 输出重定向
Linux 文件默认打开了三个文件描述符, 标准输入、标准输出和标准错误
实现重定向的步骤是在调用
exec()
之前关闭标准输出文件描述符STDOUT_FILENO
, 然后打开重定向的目标文件, 之后exec()
所调用程序的标准输出就会重定向到指定文件中了
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
int rc = fork();
if (rc < 0) { // fork 失败
fprintf(stderr, "fork failed");
exit(1);
}
else if (rc == 0) { // 子进程
close(STDOUT_FILENO);
open("./newfile.txt", O_WRONLY);
char* args[3];
args[0] = strdup("wc");
args[1] = strdup("p3.c");
args[2] = NULL;
execvp(args[0], args);
}
else { // 父进程
wait(NULL); // 等待子进程结束
}
return 0;
}
/*
output: cat newfile.txt
29 71 647 p3.c
*/