一.进程关键概念
-
什么是程序,什么是进程,有什么区别?
-
程序是静态的概念,gcc xxx.c -o pro
磁盘中生成pro文件,叫做程序。
-
进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。
-
-
如何查看系统中有哪些进程?
-
适用ps指令查看
实际工作中,配合grep来查找程序中是否存在某一个进程。
-
使用top指令查看,类似windows任务管理器
-
-
什么是进程标识符?
-
每个进程都有一个非负整数表示的唯一ID。叫做PID,类似身份证。
-
PID=0:称为交换进程(swapper);作用:进程调度
PID=1:init进程;作用:系统初始化
-
编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符。
-
-
什么叫父进程,什么叫子进程?
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。
-
C程序的存储空间是如何分配?
二.进程创建实战
-
使用fork函数创建一个进程:pid_t fork(void);
- fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,forK就会复制一份原来的进程即就是创建一个新进程,我们称子进程,而原来的进程我们称为父进程,此时父子进程是共存的,他们一起向下执行代码。
-
就是调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。
-
fork的返回值问题:
在父进程中,fork返回新创建子进程的进程ID;
在子进程中,fork返回0;
如果出现错误,fork返回一个负值;
getppid():得到一个进程的父进程的PID;
getpid():得到当前进程的PID;
**注意:**在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
-
实例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> /** *最基础的fork例子 **/ int main(int argc, char const *argv[]) { pid_t pid; //判断1 if ((pid=fork()) < 0) { perror("fork error"); } //判断2 else if (pid == 0)//子进程 { printf("child getpid()=%d\n", getpid()); } //判断3 else if(pid > 0)//父进程 { printf("parent getpid()=%d\n", getpid()); } return 0; }
解析:
两个判断的代码都执行了,这是非常不可思议的,但fork函数确实实现了这样的功能。也就是在fork函数后面的代码都会执行2遍。
这就是为什么两个判断都会被执行的原因。在来梳理-下成功fork的执行流程
第一步: pid=fork(),如果成功那么pid就有一个非0正值。否则返回-1。
第二步: 因为pid>0,所以进入判断3。这是在父进程。
第三步: 父进程的代码执行完了,程序又会把fork后面的函数再执行一遍,此时pid的值变为0,所以进入判断2。
-
写时拷贝
-
fork创建一个子进程的一般目的
一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char const *argv[]) { int data; pid_t pid; while(1){//用while(1)来一直请求 printf("please input data:\n"); scanf("%d",&data); if(data == 1){//用户请求满足 pid = fork(); if(pid > 0){//parent process父进程 } if(pid == 0){//subprocess子进程 while(1){ printf("connect success!pid = %d\n",getpid()); sleep(3); } } }else{ printf("wait,do nothing!\n"); } } return 0; }
-
vfork函数
-
vfork和fork的区别
- fork() 子进程拷贝父进程的数据段,代码段;vfork() 子进程与父进程共享数据段。
- fork() 父子进程的执行次序不确定;vfork():保证子进程先运行,当子进程调用exit退出后,父进程才 执行。
-
实例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = vfork(); int cnt; if(pid > 0){//parent process while(1){ printf("cnt = %d\n",cnt); printf("this is parent process,pid = %d\n",getpid()); sleep(1); } } if(pid == 0){//subprocess while(1){ printf("this is subprocess,pid = %d\n",getpid()); sleep(1); cnt++; if(cnt == 3){ exit(0); } } } return 0; }
子进程执行3次退出后,父进程才开始执行,且因为共享数据,cnt的值被改变。
-
三.退出
-
退出方式
-
正常退出
- main()函数调用return。
- 进程调用exit(),标准C库。
- 进程调用_exit()或者_Exit(),属于系统调用。
- 进程最后一个线程返回,最后一线程调用pthread_exit。
-
异常退出
- 调用abort。
- 当进程收到某些信号时,如Ctrl+C。
- 最后一个线程对取消(cancellation)请求做出响应。
-
-
为什么要等待子进程退出
- 防止僵尸进程,造成内存泄漏。
- 父进程要管理子进程,所以父进程交代给子进程的任务完成的如何,都需要知道,如,子进程运行完成,运行结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
-
僵尸进程
如果子进程的退出状态不被收集,子进程会变成僵死进程(僵尸进程)。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = vfork(); int cnt; if(pid > 0){//parent process while(1){ printf("cnt = %d\n",cnt); printf("this is parent process,pid = %d\n",getpid()); sleep(1); }//父进程中没有收集子进程状态的函数 } if(pid == 0){//subprocess while(1){ printf("this is subprocess,pid = %d\n",getpid()); sleep(1); cnt++; if(cnt == 3){ exit(0); } } } return 0; }
子进程pid为49017。
Z是单词(zombie)的缩写,意思是僵尸。
-
收集子进程退出信息的函数wait和waitpid
-
wait函数
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
-
status:是一个整型数指针。
指针为空:不关心退出状态;
指针非空:子进程退出状态放在它所指向的地址中。
-
函数作用:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
-
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),程序出错时wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID。
-
wait(status),status非空。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = vfork(); int cnt; int status = 0; if(pid > 0){//parent process while(1){ wait(&status); printf("child process exit data = %d\n",WEXITSTATUS(status)); //status的输出需要系统定义的宏WEXITSTATUS来解码 printf("cnt = %d\n",cnt); printf("this is parent process,pid = %d\n",getpid()); sleep(1); } } if(pid == 0){//subprocess while(1){ printf("this is subprocess,pid = %d\n",getpid()); sleep(1); cnt++; if(cnt == 3){ exit(0); } } } return 0; }
得到exit(0);的返回值0。
且不存在子进程的僵尸进程。
-
关于退出返回值的一些宏
-
-
waitpid
-
头文件和函数体
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options); int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
-
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,wait使调用者阻塞,waitpid 可以使调用者不阻塞。
-
PID:
pid > 0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid == -1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid == 0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid < -1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
**status:**是一个整型数指针,和wait一样。
**options:**options提供了一些额外的选项来控制waitpid。
waitpid(pid,&status,WNOHANG);
-
-
孤儿进程
-
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。
Linux为了避免孤儿进程过多,init进程收留孤儿进程,变成孤儿进程的父进程(init进程为系统初始化进程,进程ID为1)。 -
实例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); int cnt; int status = 0; if(pid > 0){//parent process printf("this is parent process,pid = %d\n",getpid()); sleep(2); } if(pid == 0){//child process while(1){ printf("this is child process,pid = %d",getpid()); printf("my father pid = %d\n",getppid()); sleep(1); cnt++; if(cnt == 3){ exit(0); } } } return 0; }
开始子进程的父进程pid是50348,之后父进程结束,子进程继续运行,变成孤儿进程,被init收留,此时父进程的pid为1。
孤儿进程运行状态为S(后台运行),Ctrl+C不能退出,需要kill -9 PID来推出孤儿进程。
-
四.exec族函数
-
exec族函数
-
**功能:**我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
-
**函数族:**exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe。
-
函数原型:
#include <unistd.h> extern char **environ; 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[]);
-
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
-
参数说明:
path:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
-
-
带l的一类exac函数
-
带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。
-
实例:
//文件echoarg.c #include <stdio.h> int main(int argc,char *argv[]) { int i = 0; for(i = 0; i < argc; i++) { printf("argv[%d]: %s\n",i,argv[i]); } return 0; }
//文件execl.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execl\n"); if(execl("./echoarg","echoarg","abc",NULL) == -1) { printf("execl failed!\n"); perror("why"); } printf("after execl\n"); return 0; }
执行结果:
其他示例:
调用ls: execl("/bin/ls","ls",NULL); 调用ls -l: execl("/bin/ls","ls","-l",NULL); 获取系统时间: execl("/bin/date","date",NULL);
-
-
带p的一类exac函数
-
带p的一类exac函数,包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin。
-
示例:
execl("/bin/date","date",NULL); 等价于: execlp("date","date",NULL);
-
-
带v不带l的一类exac函数
-
带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL}; 。
-
示例:
char *argv[] = {"ps","-l",NULL}; execv("/bin/ps",argv); 等价于: execvp("ps",argv);
-
-
system函数
-
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
-
system源码
#include <sys/types.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL){ return (1); } if((pid = fork())<0){ status = -1; }else if(pid = 0){ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); -exit(127); //子进程正常执行则不会执行此语句 }else{ while(waitpid(pid, &status, 0) < 0){ if(errno != EINTER){ status = -1; break; } } } return status; }
-
有system源码可知,system其实是简单粗暴的exec;system是exec的封装版。
-
system和exec的区别:
- system()和exec()都可以执行进程外的命令,system是在原进程上开辟了一个新的进程,但是exec是用新进程(命令)覆盖了原有的进程。
- system()和exec()都有能产生返回值,system的返回值并不影响原有进程,但是exec的返回值影响了原进程。
-
-
popen函数
-
函数原型
#include <stdio.h> FILE *popen(const char *command, const char *type);
-
**返回值:**如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断。
-
**函数作用:**popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程这个进程必须由 pclose() 函数关闭。
-