进程控制
进程操作
创建一个进程
进程是系统中基本的执行单位。Linux环境下使用 fork 函数创建一个新进程,函数原型为:
#include<unistd.h>
pid_t fork(void);
- Linux系统中任何一个进程都是由其他进程创建的,创建新进程的进程,即调用fork函数的进程就是父进程。
- 返回值:
对于父进程,fork 函数返回新创建的子进程的ID;
对于子进程,fork 函数返回0。由于系统的0号进程是内核进程,所以子进程的进程号不可能是0。由此区别父进程和子进程;
如果出错,fork 函数返回-1。 - 特点: fork函数会创建一个新的进程,并从内核中为此进程得到一个新的可用进程ID.之后为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,系统中又多了一个进程,这个进程和父进程一模一样,两个进程都要接受系统的调度。 由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork 函数中,等待返回。因此,fork 函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
- 案例: 创建一个新进程,并打印相关信息
#include<unistd.h>
int main(){
pid_t pid;
pid=fork();
if(pid<0){
perror("fail to fork\n");
exit(1);
}else if(0 == pid){
printf("this is chiuld,pid is %u\n",getpid());
}else{
printf("this is parent,pid is %u,child pid is %d\n",getpid(),pid);
}
return 0;
}
//运行结果
- 说明:由于创建的新进程和父进程在系统看来是地位平等的两个进程,所以运行机会是一样的,读者不能对其执行顺序进行假设,先执行哪一个进程取决于系统的调度算法,如果想确保运行顺序,需要额外的操作。
父子进程的共享资源
- 子进程和父进程:子进程完全复制了父进程的地址空间的内容,包括堆栈段和数据段的内容。子进程并没有复制代码段,而是和父进程共用代码段。这样做是合理的,由于子进程可能执行不同的流程,因此会改变数据段和堆栈。但是代码段是只读的,不存在修改的问题,因此父子进程可以公用这些代码段。
- 子进程继承的资源情况:
- 写时复制:现在的Linux内核实现fork函数时往往实现为子进程先于父进程共享代码段、数据段 和堆栈段,当子进程修改这些共享内容时,复制才会发生,内核才会给子进程分配进程空间将父进程的内容复制过来,继续后面的操作。这样的实现更加合理,对于一些只是为复制自身完成一些工作的进程来说,这样做的效率会更高。这就是现代操作系统中一个重要的概念一“写时复制”的一个重要体现。
fork函数的出错情况
fork函数出错会返回-1,有两种情况会导致出错:
- 系统中已经有太多的进程存在;
- 调用fork函数的用户的进程太多了。
创建一个共享空间的子进程
进程在创建一个新的子进程之后,子进程的地址空间完全和父进程分开。父子进程是两个独立的进程,接受系统调度和分配系统资源的机会均等。因此父进程和子进程更像是一对兄弟。如果父子进程公用父进程的地址空间,则子进程就不是独立于父进程的。
- Linux使用vfork函数创建一个公用父进程地址空间的子进程,函数原型为:
#include<unistd.h>
pid_t vfork();
- vfork 和fork函数的区别有:
(1)vfork函数产生的子进程和父进程完全共享地址空间,包括代码段、数据段和堆栈段,子进程对这些共享资源所做的修改,可以影响 到父进程。由此可知,vfork 函数与其说是产生了一个进程,不如说是产生了一个线程。
(2)vfork函数产生的子进程一定比父进程先运行,也就是说父进程调用了vfork 函数后会等待子进程运行后再运行。
- 子进程改变的变量值在父进程中同样可以检查到!!!
在函数内部调用vfork函数
- 警告: 不要在任何函数(非main函数)中调用 vfork 函数;
- 案例:
#include<stdio.h>
#include<unistd.h>
int f1(){
vfork();
return 0;
}
int f2(int a,int b){
return a+b;
}
int main(){
int c;
f1();
c=f2(1,2);
printf("%d\n",c);
return 0;
}
程序运行出现段错误!!
- 原因: 左边这张图说明 vfork 之后产生了一个子进程,并且和父进程共享堆栈段,两个进程都要从 f1 函数中返回。由于子进程先于父进程运行,所以子进程从f1 函数中返回,并且调用 f2 函数。其栈帧覆盖了原来 f1 函数的栈帧。当子进程结束运行,父进程运行时,就出现了右图的情景,父进程需要从 f1 函数中返回,但是 f1 函数的栈帧已经被 f2 函数所取代,因此就会出现父进程返回出错,发生段错误的情况。
- 因此,使用 vfork 后,子进程对父进程的影响是巨大的,其同步措施势在必行,由此也可以体会到线程同步的重要性!
退出进程
当一个进程需要退出时,需要调用退出函数,这个退出函数会深入内核注销掉进程的内核数据结构,并且释放进程的资源。Linux环境下使用exit函数退出进程,其函数原型如下:
#include<stdlib.h>
void exit(int status);
注:c 程序中的 return 会被编译器翻译为调用 exit 函数,例如:return 1 -》exit(1);
使用exit函数检查进程出错信息
通常 exit 函数的参数是该程序的退出状态,如果正常退出,参数为0;如果异常退出,参数为非零。用户也可以将ermo变量作为参数传递给exit函数,这样可以在程序退出后检查程序退出的原因。
exit函数与内核函数的关系
exit函数是一个标准的库函数,其内部封装了Linux系统调用__exit 函数。两者的主要区别在于exit函数会在用户空间做一些善后工作, 例如,清理用户的I/O缓冲区,将其内容写入磁盘文件等,之后才进入内核释放用户进程的地址空间:而 __exit 函数直接进入内核释放用户进程的地址空间,所有用户空间的缓冲区内容都将丢失。
设置进程所有者
- Linux环境下使用setuid函数改变一个进程的实际用户ID和有效用户ID,函数原型为:
#include<unistd.h>
int setuid(uid_t uid);
int seteuid(uid_t uid)
int setgid(gid_t gid);
int setegid(gid_t gid);
调试多进程(gdb调试)
- 设置跟踪流
在进程调用了 fork 函数后,gdb 可以通过设置跟踪流选项的方式指定跟踪父进程还是子进程,其设置方式如下:
set follow-fork-mode [parent | child]
此外还可以使用detach-on-fork 参数,指示gdb在进程调用fork 函数之后是否断开(detach)某个进程的调试,或者都交由gdb控制。
set detach-on-fork [on | off]
如果选项中选择on,则断开调试fllow-fork-mode指定的进程。如果选项中选择off,gdb将控制父进程和子进程。follow-fork-mode指定的进程将被调试,另一个进程置于暂停状态。
- 使用gdb的attach命令
第2种方法是使用gdb的atach命令。gdb调试器中的atch命令可以调试一个已经运行的进程,在进程调用fork函数后可以使用atch命令调试子进程。前提是要知道子进程的进程ID,并且子进程能够等待调试的开始。为了解决这两个问题,用户需要在待调试的子进程代码中添加一些辅助调试的代码。