一.进程相关概念
1.1 什么是程序?什么是进程?有什么区别?
- 程序是静态的概念,gcc xxx.c -o pro,磁盘中生成的pro,叫做程序
- 进程是程序的一次运行活动,就是程序跑起来了,系统中就多了一个进程
1.2 如何查看系统中有哪些进程?
- 使用ps指令查看
实际工作中,配合grep 来查找程序中是否存在某一进程
ps -aux:查看所有进程
ps -aux|grep xxx:用grep管道过滤出我们想要的进程信息 - 使用top指令查看,类似于windows的任务管理器
1.3 什么是进程标识符?
-
每个进程都有一个唯一的非负整数表示ID
叫做pid,类似身份证pid = 0;称为交换进程(swapper)
作用—进程调度
pid = 1;init进程
作用——系统初始化 -
编程调用getpid函数获取自身的进程标识符
getppid获取父进程的进程标识符
1.4 父进程和子进程
- 如果进程A创建了进程B
那么,A叫做父进程,B叫做子进程,父子进程是相对的概念。
1.5 C语言程序的存储空间是如何分配的?
低地址——高地址
-
正文段:
CPU执行的机械指令部分,可共享,只读。
-
初始化数据段:
数据段,明确赋值的变量
-
非初始化数据段:
BSS段。
-
堆:
进行动态存储分配(malloc等),根据习惯位于BSS段和栈之间
-
栈:
自动变量以及函数调用每次所需保存的信息都存放在此段中
-
命令行参数和环境变量
二.子进程
2.1 fork()创建子进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-
fork()函数调用成功,返回两次
返回值为0,代表当前进程是子进程
返回值是非负数,代表当前进程是父进程调用失败,返回-1
-
fork()使用
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
pid_t pid1;
pid_t pid2;
pid1 = getpid();
printf("pid before fork is %d ",pid1);
fork();
pid2 = getpid();
printf("pid after fork is %d ",pid2);
if(pid1 == getpid()){
printf("this is father,father pid is %d\n ",getpid());
}else{
printf("this is child,child pid is %d\n",getpid());
}
return 0;
}
pid before fork is 170985 pid after fork is 170985 this is father,father pid is 170985
pid before fork is 170985 pid after fork is 170986 this is child,child pid is 170986
- 结论:fork()创建子进程后,fork前面只有父进程再跑,fork后面有两个进程在跑。
并且,fork()返回值,在父进程中,是其子进程的pid。
而 , fork()返回值,在子进程中,是0。
2.2 进程创建时,发生了什么?
- 早期,创建子进程,会把程序存储空间里的正文,初始化数据,堆,栈都拷贝一份到子进程
后期,技术更新,只做写实拷贝,只对发生变动的部分进行拷贝,没有变动的部分是共享存储空间的。 - 对于拷贝部分的变量,在子进程中发生变动,不会影响在父进程中的值,两个进程中的变量地址是不一样的。
2.3 fork()创建子进程的实际应用场景
2.3.1 fork()目的
- 一个父进程希望复制自己,使父,子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端请求的服务请求。当请求到达时,父进程调用fork(),使子进程处理此请求。父进程则等待下一个服务请求到达。
- 一个进程要求执行一个不同的程序。这对shell是常见的情况,在这种情况下,子进程从fork()返回后,立即调用exec。
2.4 vfork() 创建子进程
- 区别一:vfork() 直接使用父进程存储空间,不拷贝。
- 区别二:vfork() 保证子进程先运行,当子进程调用exit退出后,父进程才执行。
三.进程的退出
3.1正常退出
- main 函数调用 return
- 进程调用exit(),标准C库
- _exit()或者 _Exit(),属于系统调用
- 进程最后一个线程返回
- 最后一个线程调用pthread_exit()
3.2 异常退出
- 调用abort
- 当进程收到某些信号,如ctrl+c
- 最后一个线程对取消(cancellaton)请求做出响应
3.3 wait 或waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
- 不管程序如何终止,最终都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器。
- 对于,上述任意一种终止情形,我们都希望终止进程通知其父进程它是如何终止的。对于三个终止函数(exit,_exit,_Exit),实现这一点的方法是,将其退出状态,最为参数传递给函数,如exit(0),0是状态码,表示正常退出。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意情况下,该终止进程的父进程,都能用 wait 或 waitpid 函数取得其终止终止状态。
3.4 父进程等待子进程
3.4.1 为什么等待子进程退出?
- 创建子进程的目的是为了完成特定任务,所以肯定要关注子进程任务进行的结果。
3.4.2 子进程退出状态收集
- 父进程等待子进程退出,并收集子进程的退出状态
- 子进程退出状态不被收集,会变成僵尸进程(zombie)
- wait(int *status),status整型指针,用于接收子进程的状态码,如果status为空,表示不关心退出状态,不为空,则将退出状态码,放到status里面,然后可以用相应的宏,算出状态码的值,注意此时参数是整型数。
- status状态码有关的宏
判断状态宏 返回值 获取终止信号编码宏
WIFEXITED(wstatus) 正常终止为真 WEXITSTATUS(wstatus)
WIFSIGNALED(wstatus) 异常终止为真 WTERMSIG(wstatus)
WIFSTOPPED(wstatus) 子进程暂停为真 WSTOPSIG(wstatus)
- 当父进程调用子进程时,使用wait等待子进程退出,如果所有子进程还在运行,则父进程保持阻塞,其中一个子进程终止,子进程立即返回终止状态,父进程wait获取其终止状态。如果没有任何子进程,wait会立即出错返回。
- wait()回使调用者阻塞,而waitpid()不会阻塞。
- pid_t waitpid(pid_t pid, int *wstatus, int options)有关的宏 和 参数
pid参数:
pid == -1 等待任一子进程,和wait等效
pid >0 等待进程id与pid相等的子进程
pid == 0 等待其组id,等于调用进程组id的任一子进程
pid < -1 等待其组id等于pid绝对值的任一子进程
option常量:
常量 说明
WCONTINUED 若支持作业控制,指定子进程暂停后仍在继续,且状态未报告,则返回其状态
WNOHANG 若指定子进程不是立即可用,则不阻塞,此时返回值为0
WUNTRACED 若支持作业控制,指定子进程处于暂停,但其状态还未报告过,则返回其状态
宏:WIFSTOPPED(wstatus)确定返回值对应于一个子进程。
3.4.3 孤儿进程
- 父进程不等待子进程退出,在子进程之前就结束,此时子进程叫做孤儿进程
- Linux为了避免出现过多的孤儿进程,init进程会收留孤儿进程,变成孤儿进程的父进程。
四. exec 族函数
4.1 作用
- fork创建子进程后,通常会调用exec函数去执行另一个程序,此时该进程完全被替换成新程序(内容会抹除),exec函数不会创建新进程,所以不会改变进程ID。
4.2 exec函数定义
4.2.1 功能:
在调用进程内部,执行一个可执行文件,可以是二进制文件,也可以是shell脚本
4.2.2 函数族:
execl execlp, execle, execvp, execvpe
4.2.3 函数原型:
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *pathname, const char *arg, ...);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
4.2.4 返回值:
成功,无返回值;失败,errno并返回-1,从调用点继续执行
4.2.5 参数:
- path:文件路径
- arg:可执行程序参数,第一个参数是可执行程序的名字,没有带路径且以NULL结束
- file:含/,则为路径名,否则为PATH环境变量,在指定目录搜寻文件
- exec后缀的意义:
l:每个可执行程序的参数,都为单独的一个参数,并且以NULL结尾
p:当第一个参数只有文件名时,可以通过环境变量(需要配置环境变量),自动找到目标文件;使用echo $PATH 可查看 当前环境变量
v:参数部分构造一个指针数组,将该数组的指针(即二级指针),作为参数,传递给exec函数
e:传递一个指向环境变量字符串指针数组的指针,和带v类似。
4.2.6应用举例
- 主进程不断或取用户输入,当如为1时,fork()创建子进程,子进程调用execl,这的可执行程序可随意替换成自己需要的文件,取代子进程,当调用成功时,可执行程序return返回,父进程wait接收到终止信号,父进程阻塞状态取消,开始执行wait后面的代码。当调用失败时,exec返回-1到子进程,子进程继续执行,父进程则处于阻塞状态,继续等待子进程终止。
- 代码:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
pid_t pid;
int data = 0;
while(1){
printf("please input a data:");
scanf("%d",&data);
getchar();
if(data == 1){
pid = fork();
int *status = (int*)malloc(sizeof(int));;
if(pid > 0){
wait(status);
printf("change!!!\n");
}else if(pid == 0){
execl("./testconfig1","testconfig1","./test1.config",NULL);
perror("why:");
exit(0);
}else{
printf("fork error\n");
}
}
}
return 0;
}
五.system()函数
5.1原型
#include <stdlib.h>
int system(const char *command);
- system()的返回值:
成功,则返回进程状态值;
当shell不能执行时,返回127;
失败,返回-1; - system()的作用是,调用可执行文件,但是没有exec那么多参数,非常好用
5.2源码
#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()函数,其实就是调用exec函数的一个封装,本质上时fork()一个子进程,子进程调用exec执行可执行文件。和exec不同的是,system执行成功,会返回,继续执行下面的代码。
5.3 system()应用实现
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
int pid;
pid = fork();
int *status = (int *)malloc(sizeof(int));
if(pid > 0){
wait(status);
}else if(pid == 0){
system("./testconfig1 test1.config");
printf("done!!\n");
exit(0);
}else{
printf("fork error\n");
}
return 0;
}
ztj@ubuntu:~/part2/PID$ ./a.out
done!!
六. popen()函数
6.1原型
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
- command:指令
- type:类型参数。r/w 可读/可写
- 使用pclose()关闭
6.2作用
- 与sysytem()比较,好处是,可以获取运行的输出结果
- 与fopen比较,可以执行命令
- 调用fopen返回一个流,可以使用fread/fwrite 对其进行文件操作。