操作系统 ---多进程,创建进程,结束进程
什么是进程 (Process)
- 进程是在执行过程中的程序, 储存在内存(memory) 中
- 一个在执行过程中的程序需要
- program code text (程序代码)
- program counter
- register content
- stack content (function parameter return address, local variables)
- data (global variables)
- heap (dynamically allocated memory)
- …
- 以上所有这些资源组成了一个进程
多进程 Multiprocessing
- 一个CPU同一时间只能运行一个进程
- 当一个进程等待外部资源时(如进行I/O操作), CPU实际处于空闲状态
- 所以为了最大效率利用CPU, CPU可以不断的切换不同的进程, 这个过程就是多进程并发
- 在多进程中, 进程被分为不同的状态
进程的状态
- 进程分为以下几个状态
- New or Created: 进程创建完毕
- Runnable or Ready: 进程准备就绪,等待分配CPU进行执行
- Running: 正在运行
- Blocked:进入阻塞状态, 通常进行I/O操作
- Terminated: 进程结束
- Suspended: 当内存满时, OS会将一些在Ready状态或者Block状态的进程放进hard drive,然后把内存空间让给优先级更高的进程, 被放进hard drive的进程就处于suspended状态
PCB (Process Control Block)
- 储存进程状态的数据结构是 Process Control Block
- 下图是PCB中储存的信息
上下文切换 (Context Switch)
- 当操作系统切换进程时, 被切换掉的进程的状态会被保存进对应的PCB,然后加载需要执行的进程的PCB
这个过程就是context switch
Queues
- 在Ready状态和在Block状态的进程会被放进queues
Ready Queue
- 将所有在Ready状态的PCB用linked list链接起来, 组成Ready Queue
- Scheduler根据不同的调度策略从queue中选一个放入CPU执行
Device Queue
- 所有请求资源的进程会被放进device queues
- 有很多device queue, 不同的device有不同的queue,比如disk queue,printer queue
创建进程
PID — Process ID
- 每一个进程都有一个PID
- PPID表示parent process的PID
- PID >= 0
- PID 0是process scheduler
- PID 1是"init" process, “init” process is invoked by kernel at the end of the boot procedure. it never dies it is a normal user process not a system process within kernel (in MacOS, this process is
called “launchd”). All other processes are children processes of “init” process.
fork() 函数
fork( )函数的作用
- system call fork() 负责创建新的进程
- 当父进程 调用 fork(). 父进程会创建一份自己的copy, 此时系统中就有两个进程,
- copy的意思是子进程的堆和栈和父进程是完全相同的。在子进程创建完成时,子进程和父进程共享内存
- 但是当共享的内存要被写入时(不管是父进程还是子进程)这块区域就会从父进程的进程空间复制到子进程,然后再执行写入。这就是通常说的copy on write,节省不必要的内存消耗
- 两个进程会继续并发执行fork()之后的代码, 但是父进程和子进程是相互独立的
int main(void)
{
pid_t pid;
int i;
char buf[BUF_SIZE];
fork();
pid = getpid();
for (i = 1; i <= MAX_COUNT; i++) {
sprintf(buf, "This line is from pid %d, value = %d\n", pid, i);
write(1, buf, strlen(buf));
}
return 0;
}
- 上面 for loop里的代码会被parent和child进程同时执行
- 下图是执行结果,可以看到两个进程在并发执行
fork( )的返回值
- fork 函数的返回值是一个int, 表示PID, 通过返回值我们可以区分父进程和子进程
- 在父进程中, fork会返回child process的PID
- 在子进程中, fork会返回 0
- 如果返回值小于0, 则说明进程创建失败
- 子进程可以调用getppid得到parent的PID
- 下面是利用fork返回值区分父进程和子进程的例子
#include <stdio.h>
#include <sys/types.h>
#define MAX_COUNT 500
void ChildProcess(void); /* child process prototype */
void ParentProcess(void); /*parent process prototype */
void main(void)
{
pid_t pid;
pid = fork();
if (pid == 0) {
ChildProcess();
}
else {
ParentProcess();
}
}
void ChildProcess(void)
{
int i;
for (i = 1; i <= MAX_COUNT; i++) {
printf(" This line is from child, value = %d\n", i);
}
printf(" *** Child process is done ***\n");
}
void ParentProcess(void)
{
int i;
for (i = 1; i <= MAX_COUNT; i++) {
printf("This line is from parent, value = %d\n", i);
}
printf("*** Parent is done ***\n");
}
多次调用 fork( )
exec系列函数
- fork出来的子进程和父进程代码完全一样,但是很明显不同的进程代码资源等都是不同的,这是因为一般在调用fork之后还会调用 exec系列函数
- exec系列函数在执行时会首先清空当前进程(调用exec函数的进程)的栈和堆等内存空间。然后创建新的空间。但是进程的pid和父进程等信息不会变。
- 它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了PID外,其他全部被新程序的内容替换了
/*
以下三个函数的第一个参数是可执行文件的路径名或者可执行文件名, 后面是可执行程序需要的argument list, 需要注意的是最后一个argument必须是NULL.
*/
int execl(const char *pathname, const char *arg, ...)
//envp[]包含所需要的环境变量, 需要注意的是是数组最后一个元素必须是NULL.
int execle(const char *pathname, const char *arg, ..., char *const envp[])
int execlp(const char *filename, const char *arg, ...)
/*
以下三个函数的第一个参数是可执行文件的路径名或者可执行文件名, 后面是可执行程序需要的arguments数组, 需要注意的是是数组最后一个元素必须是NULL.
*/
int execv(const char *pathname, char *const argv[])
int execve(const char *pathname, char *const argv[], char *const envp[])
int execvp(const char *filename, char *const argv[])
Example:
exec.c
int main() {
pid_t pid;
pid = fork();
if (pid == 0 ) {
printf("This is the child process %d\n", getpid());
//child.c的可执行文件
execlp("./child.out", "child", NULL);
printf("This line will not be printed, because execlp syscall\n");
}
else {
printf("This is the parent process %d\n", getpid());
}
return 0;
}
child.c
int main() {
printf("This is process %d running child.c", getpid());
return 0;
}
- 首先在exec.c中创建一个子进程,这个子进程会调用execlp, 在execlp函数中放入child.c的可执行文件child.out,子进程会运行child.c程序,而execlp之后的代码则不会被执行,因为从父进程复制过来的代码已经被execl清空
结束进程
孤儿 (Orphan) 进程
- 当父进程先于子进程结束时, 这个父进程的所有子进程被叫做orphan
- 当一个进程结束时,kernel会遍历所有活跃的进程,检查中止的进程是否是任何活跃进程的父进程(通过检查PCB中的pointer to parent process ). 如果是, 则该子进程的父进程会被改为 “init”(PID = 1). 这样就保证所有的进程都有父进程, 避免orphan进程的出现
僵尸 (Zombie) 进程
什么是僵尸进程
- 当子进程先于父进程结束时, 这个子进程被叫做Zombie
- 当进程结束时, 系统分配给进程的资源(如register, stack, data等)会被回收, 此时的进程处于死亡状态, 没有可执行代码不占用任何资源也不能被调度。 但是死亡进程会在kernel中的process table保留一个位置,用来记录退出时的状态供其他进程收集.
- 这个死亡进程需要父进程来清除它在proces table中的位置
- 但是父进程不知道子进程有没有中止,所以无法清除这个entry, 此时子进程就成为了Zombie
- 僵尸进程会占用process table中的位置,如果不清理会将process table被塞满导致不能再创建新的进程
- 调用wait或者waitpid函数可以解决zombie问题
- 很多情况下不调用wait或者waitpid也能解决僵尸进程,因为父进程结束之后,子进程的父进程会变为 “init” 进程, 而"init"进程会调用wait处理僵尸进程,除非父进程处于循环状态无法结束
如何解决僵尸进程 — wait ( ) and waitpid( )
wait 函数原型
#include <sys/types.h>
#include <sys/wait.h>
/*
* parameter: int *status: 用于获取僵尸子进程的状态信息,
* 通过宏(比如WEXITSTATUS等)分析状态信息
* return value: 僵尸子进程的PID,失败返回-1
*/
pid_t wait(int *status);
- 解决Zombie 进程的方法是在父进程中调用wait( ) system call
- 当子进程结束时,内核会向其父进程发送SIGCHLD信号, 对于这个信号父进程可以选择忽略或者提供信号处理程序
- 系统默认是忽略这个信号的,而调用wait() 可以处理这个信号
- wait函数首先查看有没有僵尸进程,如果有则立即返回并取得该子进程的终止状态 如果没有 wait 函数会阻塞父进程直到此父进程的任意(注意是任意)子进程结束,
- 当子进程结束后,wait函数会清除子进程在process table中的位置,获取子进程结束状态(用status指针记录),给父进程发信号跳出block状态
//wait Example
#include <stdio.h>
void forkexample()
{
// child process because return value zero
if (fork() == 0) {
sleep(10);
printf("Hello from Child!\n");
}
// parent process because return value non-zero.
else {
/*
wait( ) will block the parent until a child terminate
when child terminate, kernel will send a signal to parent
*/
wait(NULL);
printf("Hello from Parent!\n");
}
}
int main() {
forkexample();
return 0;
}
waitpid 函数原型
#include <sys/types.h>
#include <sys/wait.h>
/*
* parameter:
* pid_t pid: 需要清除的子进程pid
* -1 表示不等待某个特定PID的子进程而是回收任意一个子进程
* int *status: 用于获取僵尸子进程的状态信息
* int options: 0表示父进程会阻塞等待子进程结
* WNOHANG表示父进程要非阻塞式的回收子进程。
* 如果父进程waitpid时子进程已结束, 则waitpid成功,返回值子进程PID;
* 如果父进程waitpid时子进程尚未结束,则父进程立刻返回(非阻塞),返回值为0(回收不成功)
* return value: 僵尸子进程的PID,失败返回-1
*/
pid_t waitpid(pid_t pid, int *status, int options);
- waitpid可以清除具体某一个子进程
- waitpid可以指定父进程是阻塞式等待或者非阻塞式
- waitpid(-1, NULL, 0) 等同于 wait(NULL)
//waitpid Example
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0) {
printf("fork error");
}
else if (pid == 0) { // first child
if ((pid = fork()) < 0) // fork again
printf("fork error");
else if (pid > 0)
exit(0); // parent from second fork == first child
// second value of pid == 0
// that means this is the second child;
// the parent becomes init as soon
// as the real parent calls exit() in the statement above.
// continue executing,
// when it is done, init will get our status.
sleep(2);
printf("second child, parent pid = %ld\n", (long)getppid());
exit(0);
}
// pid > 0 case, this is the parent (the original process)
// pid is process id of the first child
if (waitpid(pid, NULL, 0) != pid) // wait (blocked) for first child
printf("waitpid error");
// continue executing,
// knowing that we’re not the parent of the second child.
exit(0);
}
以下代码表示wait所有子进程,父进程block直到所有子进程结束
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
int main(void) {
pid_t child_pid, wpid;
int status = 0;
int n=10;
//parent code (before child processes start)
for(int id=0; id<n; id++) {
if((child_pid = fork()) == 0) {
//child code
printf("in child: %d\n", getpid());
exit(0);
}
}
// the parent waits for all the child processes
while ((wpid = wait(&status)) > 0); //不断循环,直到wait返回 -1(wait失败)
// parent code (after all child processes end)
return 0;
}
如果结束进程 — exit ( )
- 调用exit ()可以中止正在运行中的进程
- exit函数会将子进程的exit status传送给父进程,分为一下三种情况
- 如果父进程设置了SA_NOCLDWAIT 或者 将SIGCHLD handler设置为SIG_IGN, status会被遗弃并且子进程会立即终止
- 如果父进程调用了wait函数,exit会将exit status传给父进程,然后子进程会立即终止
- 否则子进程会变为僵尸进程
exit ( ) 和 return 的区别
- exit是结束进程,返回父进程
- return是结束函数,返回上一层函数
- main函数的return 0等价于 exit()