进程创建
fork会在已存在的进程中创建一个新进程出来,并且当前进程作为父进程,新创建出来的进程作为子进程。当进程调用fork之后,内核会分配新的内存块和数据给子进程,并且将父进程数据内容拷贝给子进程,然后将子进程添加至系统就绪队列,系统调用fork完成之后开始返回,但是这里fork将会有两个返回值父进程返回子进程的进程ID,子进程返回0,通常用if语句来做分流。
pid_t fork(void);
那么在fork调用之后,我们是如何知道fork返回两个返回值?我们可以先来看一段代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("Before:pid is %d\n", getpid());
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
通过执行结果可以明显看出来父进程子进程以及父子进程的返回值, 但是子进程是按照父进程的模板复制出来的,为什么只打印了一次Before,这就是我们接下来说的,父子进程虽然有同一份代码,但是并不是父子进程都要执行全部,fork之前父进程独立执行,fork之后两个分别执行。谁先执行完全由调度器决定
当然,fork调用也不一定每次都成功,可能原因就是系统中的进程数太多,还有就是实际用户的进程数超过了限制。当然,创建进程不只有fork一中方法,也可以通过vfork来创建进程,vfork创建的子进程和父进程共享地址空间,并且保证子进程先执行,父进程之后执行。
进程等待
当子进程退出,父进程不管不顾,就有可能造成僵尸进程进而造成内存泄露,另外进程一旦变成僵尸进程,就无法通过kill来杀死它,还有父进程交给子进程的任务完成的如何,父进程需要知道子进程的结果如何,父进程可以通过进程等待的方式来获取子进程的退出信息,回收子进程资源。
pid_t wait(int *status);
返回值:
调用wait成功返回被等待的进程,失败返回-1,
参数:
输出型参数,获取子进程的退出状态,不关心可设置为NULL
wait可以胜任等待子进程的任务,但是我们用的更多的是waitpid:
pid_t waitpid(pid_t pid, int *status, int options);
waitpid的返回值和wait函数的返回值是一样的,但是如果设置了选项WNOHANG,waitpid发现没有已退出的子进程可以收集,就会返回0。
pid
pid为-1,等待任意一个子进程,pid>0,等待进程ID与pid相等的进程
status
wait和waitpid都有status参数,这个参数是一个输出型参数,有操作系统填充,如果填NULL,则表示不关心子进程的退出状态,否则,操作系统会将子进程的退出状态填充至status返回给父进程。但是status不能当一个普通的整形来看待,可以当做位图来看,此处不研究高16位,只看低16位。
这样用户就可以根据status来获取子进程的退出状态。
options
options设置WNOHANG,在说WNOHANG之前,可以先来了解两个概念:阻塞和非阻塞。wait默认是阻塞的,即调用wait之后如果返现没有退出的子进程,那么父进程就在此等待子进程退出,进入睡眠状态(可中断),此时就是阻塞式等待,但是设置WNOHANG之后,会设置父进程为非阻塞等待,所以没有已退出的子进程,waitpid立即返回。
#incldue <stdio.h>
#incldue <stdlib.h>
#include <string.h>
#incldue <unistd.h>
#incldue <sys/wait.h>
int main()
{
pid_t pid;
if((pid = fork()) == -1){
perror("fork");
exit(1);
}
if(pid == 0){ //child
sleep(20);
exit(2);
}
else{ //father
int st;
int ret = wait(&st);
//int ret = waitpid(pid, &st, WNOHANG);
if(ret > 0 && (st & 0x7F) == 0){ //normal
printf("child exit code:%d\n", (st>>8)&0xFF);
}
else if(ret > 0){ //signal
printf("sig code:%d\n", st&0x7F);
}
else if(ret == 0){
printf("非阻塞,没有子进程退出!\n");
}
}
}
子进程正常返回,调用exit
子进程被kill信号所杀,再打开一个终端我们可以用kill杀死子进程
ctrl+c终止进程
进程程序替换
父进程创建出子进程之后希望完成一系列动作,所以往往子进程需要其他的程序来帮助自己完成,但是子进程没有能力独立去完成,这就需要进行程序替换。子进程调用exec函数来执行另一个程序,调用exec时,进程的用户空间代码和数据完全被新程序替换,从新的程序开始执行,调用exec并不会创建新的进程,所以调用exec前后进程id并未改变。
替换函数
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
观察函数名称就可以大概知道用法,l(list)表示采用列表,v(vector)采用数组,p(path)自动搜索环境变量,e(env)自己维护环境变量。
进程退出
进程退出有三种状态,正常退出结果正确,正常退出结果不正确,异常退出,在命令行下可以通过echo $?来查看退出码,通常正常退出有三种方式,return返回,调用exit,调用_exit,异常退出ctrl+c。
#include <unistd.h>
void _exit(int status);
虽然status是int,但是仅有低8位可以被父进程所用,所以当exit(-1)时,就会发现返回值是255
#include <stdlib.h>
void exit(int status);
_exit是系统调用,exit是库函数,调用exit虽然会使用系统调用,但是在系统调用之前,exit会关闭所有打开的文件描述符,所有的缓存数据都被写入,最后进程退出。
执行exit就相当与执行return,main函数运行时会将main的返回值作为exit的参数。
小案例
我们学了从进程的创建到终止一系列操作,就会发现shell的基本原理和进程的一生有着及其相似的地方:
1.获取命令
2.解析命令
3.bash创建子进程(fork)
4.替换子进程(execvp)
5.父进程等待子进程退出(wait)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void Pares_cmd(char *buf, char *out_cmd[])
{
int i = 0;
int j = 0;
while(buf[i]){
while(buf[i] == ' '){
i++;
}
out_cmd[j++] = buf + i;
while(buf[i] != ' ' && buf[i] != 0){
i++;
}
buf[i++] = '\0';
}
out_cmd[j] = NULL;
}
void do_exec(char *out_cmd[])
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
return;
}
else if(pid == 0){
execvp(out_cmd[0], out_cmd);
exit(1);
}
else{
wait(NULL);
}
}
int main()
{
int i;
while(1){
char buf[100] = {0};
char *out_cmd[100] = {0};
printf("[zmy@zmy]$ ");
gets(buf);
Pares_cmd(buf, out_cmd);
do_exec(out_cmd);
}
return 0;
}