一、进程相关概念
1.什么是程序,什么是进程,有什么区别?
程序是静态的概念,gcc xxx.c -o pro 磁盘中生成pro文件,叫做程序。
进程是程序的一次运动活动,通俗点意思是程序跑起来了,系统中就多了一个进程。
2.如何查看系统中有哪些进程?
使用ps指令查看 ps -aux;实际工作中,配合grep来查找程序中是否存在某一个进程。
使用top指令查看,类似windows任务管理器。
3.什么是进程标识符
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。
pid = 0;称为交换进程(swapper)
作用——进程调度
pid = 1;init进程
作用——系统初始化
编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符
4.什么叫父进程,什么叫子进程?
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。.
5.C程序的存储空间是如何分配的?
可执行程序包括BSS段、数据段、代码段。
在类UNIX系统下可使用size命令查看可执行文件的段大小信息。如size a.out:
~/Desktop/MyC$ size a.out
text data bss dec hex filename
1672 600 8 2280 8e8 a.out
1.数据段存放已初始化的全局变量和静态变量,数据段属于静态内存分配。
2.BSS段(Block Started by Symbol)存放未初始化的全局变量和静态变量。
BSS段的数据是可读写的,链接器从可执行文件中得到BSS段的大小,然后申请得到这块内存空间,这块内存空间紧跟在数据段的后面。由此可知BSS段并不占用可执行文件的大小。在使用BSS段之前BSS段会自动初始化为0。所以,未初始的全局变量和静态变量在程序执行之前已经是0了。BSS段属于静态内存分配。
包含数据段和BSS段的整个区段通常称为数据区。
3.代码段:用来存放程序的代码内存空间。它的大小在程序运行前就已经确定了,并且该区域只能读不能写。在代码段中,也有可能包含了一些只读的常数变量,例如字符串常量等。
代码段和数据段在编译时已经分配了空间,而BSS段则在程序被调入内存后才分配的。因此BSS段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。需要存放在程序文件中的只有代码段和数据段(存放已初始化的全局变量和静态变量)的内容。
程序编译后生成的目标文件至少含有这三个段,这三个段的大致结构图如下所示:
可执行程序在运行时会多出两个区域:栈区和堆区。
4.栈区:由操作系统自动分配和释放 ,存放函数的参数值,局部变量的值等。每当一个函数被调用时,该函数的返回类型和一些调用的信息也会被存放到栈中。然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
小知识:
自动变量:什么是自动变量呢?自动变量就是指在函数内部定义使用的变量。只能在函数内部使用它。自动变量是局部变量,即它的作用区域是在定义它的函数内部。由于自动变量在定义它的函数的外部是不可见的,所以允许我们在这个函数外部或者其他的函数内部定义同名的变量。计算机在执行这个函数的时候,创建这个变量并为它分配内存,当函数执行完毕返回后,自动变量就会被销毁。为自动变量分配内存就是压栈,而函数返回时就退栈。
临时变量是指在未在程序开头部分声明的,待使用它时才声明类型的变量。常见的如函数中定义的变量,循环语句、条件语句中声明定义的变量。这些变量可与主程序中的变量同名,在其作用域里,主程序中的同名变量一般无法调用。并且这种变量有效存在时间是从变量声明开始到相应程序段(循环结构或函数体)结束。
5.堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员亲自用malloc()申请分配和用free()释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的 malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
最后提示一下:内存泄漏是指内存分配出去以后,你再也访问不到了。
二、创建进程函数fork的使用
pid_t fork(void);
fork函数调用成功,返回两次
返回值为0,代表当前进程是子进程
返回值非负数,代表当前进程为父进程
调用失败,返回-1
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid_t pid2;
pid = getpid();
printf("before fork pid = %d\n",pid);
fork();
pid2 = getpid();
if(pid == pid2)
{
printf("this is father pid = %d\n",pid);
}else
{
printf("this is child pid = %d\n",pid2);
}
return 0;
}
利用返回值判断:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("father pid = %d\n\n",getpid());
pid = fork();
if(pid > 0)
{
printf("this is father pid = %d\n",getpid());
}else if(pid == 0)
{
printf("this is child pid = %d\n",getpid());
}
return 0;
}
运行结果:
父进程 fork返回值为子进程的id
三、创建进程发生了什么事情
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid = getpid();
int a = 10;
printf("after fork pid = %d\n",pid);
fork();
pid2 = getpid();
if(pid2 != pid)
{
printf("this is child pid\n");
a+=10;
printf("a = %d\n",a);
}else
{
printf("father pid\n");
printf("a = %d\n",a);
}
return 0;
}
互不影响
旧的:全拷贝
新的:写时拷贝,只有在数据改变时才拷贝否则数据共享。
四实际应用场景及fork总结
应用场景一:一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达.
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data;
while(1)
{
printf("please input a data:\n");
scanf("%d",&data);
if(data == 1)
{
pid = fork();
if(pid > 0)
{
}else if(pid == 0)
{
while(1)
{
printf("do net request,pid = %d\n",getpid());
sleep(3);
}
}
}else
{
printf("do nothing\n");
}
}
return 0;
}
五、vfork创建进程
vfork函数也可以创建进程,与fork有什么区别?
1.vfork直接使用父进程存储空间,不拷贝。
2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid>0)
{
while(1)
{
printf("this is father pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0)
{
while(1)
{
printf("this is child pid = %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 3)
{
exit(0);
}
}
}
return 0;
}
运行结果:
六、进程退出
正常退出
1.Main函数调用return
2.进程调用exit(),标准C库
3.进程调用_exit()或者_Exit(),属于系统调用
补充:
1.进程最后一个线程返回
2.最后一个线程调用pthread_exit
异常推出
1.调用abort
2.当进程收到某些信号时,如Ctrl+c
3.最后一个线程对取消(cancellation)请求做出响应
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何种植的。对于三个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
推荐用 exit() 其将变量做了一些处理在退出
_exit(),_Exit()直接退出
七、父进程等待子进程退出
wait 函数
父进程等待子进程退出,并收集子进程的退出状态
子进程退出状态不被收集,变成僵死进程(僵尸进程)
僵尸进程(红框)
相关函数
1.如果所有子进程都还在运行,则阻塞。
2.如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3.如果他没有任何子进程,则立即返回出错。
status参数——是一个整型数指针
非空:
子进程退出状态放在它所指向的地址中。
空:不关心退出状态
wait(NULL):
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = fork();
if(pid>0)
{
wait(NULL);
while(1)
{
printf("this is father pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0)
{
while(1)
{
printf("this is child pid = %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 3)
{
_exit(0);
}
}
}
return 0;
}
有返回值为3:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status;
pid = fork();
if(pid>0)
{
wait(&status);
printf("child back = %d",WEXITSTATUS(status));
while(1)
{
printf("this is father pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0)
{
while(1)
{
printf("this is child pid = %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 3)
{
exit(3);
}
}
}
return 0;
}
运行结果:
非阻塞:
父子同时运行
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status;
pid = fork();
if(pid>0)
{
waitpid(pid,&status,WNOHANG);
printf("child quit child status = %d\n",status);
while(1)
{
printf("this is father pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0)
{
while(1)
{
printf("this is child pid = %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 3)
{
exit(3);
}
}
}
return 0;
}
孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的生命,此时子进程叫做孤儿进程,Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程,init的进程id = 1
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = fork();
if(pid>0)
{
printf("this is father print = %d\n",getpid());
}
else if(pid == 0)
{
while(1)
{
printf("this is child pid = %d,my father pid = %d\n",getpid(),getppid());
sleep(1);
cnt ++;
if(cnt == 5)
{
exit(3);
}
}
}
return 0;
}
运行结果:
八、exec族函数
exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
exec族函数定义:
功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何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[]数组,使用新的环境变量代替调用进程的环境变量
调用系统的date:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("this pro get system date:\n");
if(execl("/bin/date","date",NULL,NULL) == -1)
{
printf("execl failed!\n");
perror("why");
}
printf("after execl\n");
return 0;
}
带p的一类exac函数,包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin
#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("ps","ps","-l",NULL) == -1)
{
printf("execl failed!\n");
}
printf("after execl*****\n");
return 0;
}
运行结果
ubuntu:~/test/exec_test$ gcc execl_no_path.c -o execl_no_path
ubuntu:~/test/exec_test$ ./execl_no_path
before execl****
execl failed!
after execl*****
上面这个例子因为参数没有带路径,所以execl找不到可执行文件。
下面再看一个例子对比一下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execlp(const char *file, const char *arg, ...);
int main(void)
{
printf("before execlp****\n");
if(execlp("ps","ps","-l",NULL) == -1)
{
printf("execlp failed!\n");
}
printf("after execlp*****\n");
return 0;
}
运行结果
ubuntu:~/test/exec_test$ gcc execlp.c -o execlp
ubuntu:~/test/exec_test$ ./execlp
before execlp****
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1048 35976 74920 0 80 0 - 2860 - pts/4 00:00:00 ps
0 S 1048 74920 74916 0 80 0 - 7579 wait pts/4 00:00:00 bash
环境变量PATH
pwd查看当前路径
echo $PATH 查看环境变量
export PATH=$PATH:(pwd所查到的环境变量)
带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL};
下面以execvp函数为例说明:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execvp(const char *file, char *const argv[]);
int main(void)
{
printf("before execlp****\n");
char *argv[] = {"ps","-l",NULL};
if(execvp("ps",argv) == -1)
{
printf("execvp failed!\n");
}
printf("after execlp*****\n");
return 0;
}
运行结果:
ubuntu:~/test/exec_test$ gcc execvp.c -o execvp
ubuntu:~/test/exec_test$ ./execvp
before execlp****
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1048 63491 74920 0 80 0 - 2860 - pts/4 00:00:00 ps
0 S 1048 74920 74916 0 80 0 - 7579 wait pts/4 00:00:00 bash
https://blog.youkuaiyun.com/u014530704/article/details/73848573
九、system函数
sysetem()函数的返回值如下:
成功,则返回进程的状态值;当sh不能执行时,返回127;失败返回-1;
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data;
while(1)
{
printf("please input a data:\n");
scanf("%d",&data);
if(data == 1)
{
pid = fork();
if(pid>0)
{
wait(NULL);
}
if(pid == 0)
{
system("./peizhi config.txt");
}
}else
{
printf("wait,do nothing\n");
}
}
return 0;
}
system原型:https://www.cnblogs.com/leijiangtao/p/4051387.html
十、popen函数
popen比system的好处:可以获取运行结果
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
FILE *fp;
char ret[1024] = {0};
fp = popen("ps","r");
int n_read = fread(ret,1,1024,fp);
printf("read ret %d bytes = %s\n",n_read,ret);
return 0;
}