进程篇
一、进程相关概念
1.什么是程序,什么是进程,有什么区别?
1)程序是静态的概念,gcc xxx.c -o pro,磁盘中生成的pro文件,叫做程序。
2)进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。
2.如何查看系统中有哪些进程?
1)使用ps指令查看,实际工作当中,配合grep来查找程序中是否存在某个进程。比如:ps -aux|grep init 过滤掉其他进程,只显示init进程。
2)使用top指令查看,类似windows任务管理器。
3.什么是进程标识符?
1)每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。
pid = 0:称为交换进程(swapper)
作用——进程调度
pid = 1: init进程
作用——系统初始化
2)编程调用getpid函数获取自身的进程标识符。
getppid获取父进程的进程标识符。
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main()
{
//pid_t getpid(void);获取进程id号函数原型。
pid_t pid;
pid = getpid();
printf("此程序进程id号为:%d\n",pid);
while(1);
return 0;
}
运行结果:
4.什么叫父进程,什么叫子进程?
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。
5.C程序的储存空间是如何分配的?
1)当一个程序运行的时候,内存空间会给该运行程序画出一块地址,上诉就是内存给程序画出的地址。
2)那么这块地址是怎么分配的呢?
正文段
这是CPU执行的技巧指令部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器,C编译器和shell等)在存储器中也只需要一个副本,另外正文段常常是只读的,以防止程序由于意外而修改其指令。
初始化数据段
通常将此段数据成为数据段,它包含了程序中需明确地赋初值的变量,例如。C程序中任何函数之外的声明: int maxcount = 99;
未初始化数据段
通常将此段称为bss段,这一名称来源于早期汇编程序一个操作符,意思是“由符号开始的块”(block started by symbol),在程序开始执行前,内核将此段终端数据初始化为0或空指针。函数外的生命:
long sum[1000];
使此变量存放在非初始化数据段中。
栈
自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息(如某些技巧寄存器的值)都存放在栈中。然后,最近调用的函数站上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C递归函数可以工作。递归函数每次调用自身时,就用一个新的栈帧,因此一次函数调用实例中的变量集不会影响另一次函数调用实例中的变量。
堆
通常在队中进行动态村粗分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。
二、创建进程函数fork的使用
1.进程创建实战
使用fork函数创建一个进程,函数原型:
pid_t fokr(void);
fork函数调用成功,返回两次
返回值是 0,代表当前进程是子进程
返回值是非负数,代表当前进程是父进程
调用失败,返回 -1
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid_t retpid;
pid = getpid();
printf("fork之前是父进程为:%d\n",pid);
retpid = fork();
pid2 = getpid();
printf("fork之后进程为:%d\n",pid2);
if(retpid > 0 && pid == pid2){
printf("父进程走的,fork的返回值是:%d 当前进程的pid:%d\n",retpid,getpid());
}else if(retpid == 0 && pid != pid2){
printf("子进程走的,fork的返回值是:%d 当前进程的pid:%d\n",retpid,getpid());
}
return 0;
}
运行结果:
注意:
- 第一段红色框,是父进程走的,并且走完了整个代码段,然后根据父进程跟子进程不同的性质返回了不一样的值,父进程对于fork的返回值是子进程的进程ID号
- 第二段绿色框,是子进程走的,只是走了fork之后包括fork的代码,然后根据子进程和父进程的性质不同返回了不一样的值,子进程对于fork的返回值是0
三、进程创建发生了什么事情
1.来看一段代码
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
int data = 10;
printf("此程序进程id号为:%d\n",getpid());
pid = fork();
if(pid > 0 ){
printf("这里是父进程:%d\n",getpid());
}else if(pid == 0){
printf("这里是子进程:%d\n",getpid());
data = data + 100;
}
printf("data的值为:%d\n",data);
return 0;
}
运行结果:
1)父进程里面的data = 10始终都没有变,而子进程里面的data = 10变成了110。
2)所以是发生了这样的事情:
- 在早期的linux都是这样做的,fork创建新进程后是完全拷贝,新进程会把旧进程的所有内存数据都拷贝一份给自己使用。
- 后来linux内核更新,为了提高效率,for创建新进程后是写时不完全拷贝,对旧进程和新进程始终不改变的值采用共享内存的方式。
四、创建新进程的实际应用场景及fork总结
1.fork创建一个子进程的一般目的:
1)一个父进程希望复制自己,是父,子进程同时执行不同的代码段。这在网络服务进程中是常见的———父进程等待客户端的服务请求。但这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等下下一位服务请求到达。
2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main()
{
pid_t pid;
int data;
while(1){
printf("请输入一个值:\n");
scanf("%d",&data);
if(data == 1){
pid = fork();
if(pid > 0){
}else if(pid == 0){
while(1){
printf("欢迎来到子进程,我的ID号是:%d\n",getpid());
sleep(3);
}
}
}else{
printf("输入错误!\n");
}
}
return 0;
}
运行结果:
五、vfork创建进程
1.vfork函数 也可以创建进程,与fork有什么区别?
- 关键区别一:vfork 直接使用父进程存储空间,不拷贝。
- 关键区别二:vfork 保证子进程先运行,当子进程调用exit退出后,父进程才执行。
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
pid_t pid;
pid_t pid2;
pid_t retpid;
int cnt = 0;
pid = getpid();
printf("fork之前的进程ID号为:%d\n",pid);
retpid = vfork();
pid2 = getpid();
printf("fork之后的进程ID号为:%d\n",pid2);
if(pid == pid2){
printf("欢迎来到父进程,我的ID是:%d\n",getpid());
printf("cnt的值为:%d\n",cnt);
}else if(pid != pid2){
while(1){
printf("欢迎来到子进程,我的ID号是:%d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(1);
}
}
}
return 0;
}
运行结果:
1)可以看到,是子进程先运行了3次,然后exit退后后,父进程才运行。
2)因为vfork创建新进程,它是直接使用旧进程的存储空间,所以cnt返回的是3。
六、进程退出
1.正常退出
1)Main函数调用return
2)进程调用exit();标准c库
3)进程调用_exit()或者_Exit();属于系统调用
补充:
a)进程最后一个线程返回
b)最后一个线程调用pthread_exit**
2.异常退出:
1)调用abort
2)当进程收到某些信号时,如Ctrl+c
3)最后一个线程对取消(cancellation)请求做出响应**
- 不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit丶_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传递给函数。在异常终止情况下,内核(不是进程本身)产生一个提示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或者waitpid函数取得其终止状态。
七、父进程等待子进程退出
1.为啥要等待子进程退出
创建子进程的目的:是让子进程去干活。这时父进程关心子进程的活干的怎么样了,所以会等待并且收集子进程的一个退出状态。
2.父进程等待子进程退出,并收集子进程的退出状态。子进程退出状态不被收集,会变成僵死进程
3个宏可以检查该状态:
WIFEXITED(status): 若为正常终止, 则为真。此时可执行
WEXITSTATUS(status): 取子进程传送给exit或_exit参数的低8位.
WIFSIGNALED(status): 若为异常终止, 则为真。此时可执行
WTERMSIG(status): 取使子进程终止的信号编号.
WIFSTOPPED(status): 若为当前暂停子进程, 则为真。此时可执行
WSTOPSIG(status): 取使子进程暂停的信号编号
3.相关函数
pid_t wait(int *status);
status参数:是一个整数型指针
非空:子进程退出状态放在它所指向的地址中
空:不关心退出状态
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 6;
pid = fork();
if(pid >0){
wait(&status);
printf("等待子进程退出完毕,退出状态为:%d\n",WEXITSTATUS(status));
while(1){
printf("欢迎来到父进程,我的ID是:%d\n",getpid());
sleep(1);
}
}else if(pid == 0){
while(1){
printf("欢迎来到子进程,我的ID号是:%d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
运行结果:
1)如果其所有子进程都在运行,则阻塞。
2)如果一个子进程已终止,正等待父进程获取其终止状态,则取得孩子进程的终止状态立即返回。
3)如果它没有任何子进程,则立即出错返回。
4.孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程
八、exec族函数
1.exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
2.功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
3.函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
4.函数原型:
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环境变量,在它所指定的各目录中搜寻可执行文件。
5.execl函数代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("调用execl之前!\n");
if(execl("/bin/ls","ls","-l",NULL) == -1){
printf("调用execl失败!\n");
perror("why");
}
printf("调用execl之后!\n");
return 0;
}
运行成功结果:
运行失败结果:
- 所以验证了:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。并且,调用成功后用execl 找到并执行ls指令,将当前进程main替换掉,所以”调用execl之后!” 没有在终端被打印出来。
6.execlp函数代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("调用execl之前!\n");
if(execlp("pwd","pwd",NULL,NULL) == -1){
printf("调用execl失败!\n");
perror("why");
}
printf("调用execl之后!\n");
return 0;
}
运行结果:
1)直接调用pwd指令程序,没有加pwd的路径名字
2)所以,execlp能通过环境变量PATH查找到可执行文件ps
7.可执行文件路径名字和环境变量?
环境变量官方意思:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。
环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。
所以意思应该环境变量是整个操作系统的一个路径目录结构吧。当我们把某个可执行文件路径名字加到export PATH=$PATH: 。然后路径下的可执行程序就可以在整个系统目录下运行了。直接输入可执行程序名,不用加./,因为./代表当前目录。
8.execvp和execv函数代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("调用execl之前!\n");
char *argv[] = {"ls",NULL,NULL};
if(execvp("ls",argv) == -1){//如果用execv的话"/bin/ls"要加绝对路径
printf("调用execl失败!\n");
perror("why");
}
printf("调用execl之后!\n");
return 0;
}
运行结果:
九、exec族函数配合fork使用
1.先看看等下要被execl调用的程序源代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char **argv)
{
int fdSrc;
char *readBuf;
if(argc != 2){
printf("参数数据不正确!\n");
exit(-1);
}
fdSrc = open(argv[1],O_RDWR);
int size = lseek(fdSrc,0,SEEK_END);
lseek(fdSrc,0,SEEK_SET);
readBuf = (char *)malloc(sizeof(char) * size +8);
int n_read = read(fdSrc,readBuf,size);
char *p = strstr(readBuf,"SHI=");
if(p == NULL){
printf("没找到!\n");
exit(-1);
}
p = p + strlen("SHI=");
*p = '5';
lseek(fdSrc,0,SEEK_SET);
int n_write = write(fdSrc,readBuf,strlen(readBuf));
close(fdSrc);
return 0;
}
- 运行结果是把文件text.h的字符“SHI”的值改成5
2.所以接下来我们通过fork进程来调用execl函数
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int data;
while(1){
printf("请输入一个值:\n");
scanf("%d",&data);
if(data == 1){
pid = fork();
if(pid > 0){
wait(NULL);
}else if(pid == 0){
execl("okok","okok","text.h",NULL);
}
}else{
printf("输入错误!\n");
}
}
return 0;
}
运行结果:
十、system函数
int system(const char *command);
执行一个shell指令,简单粗暴
返回值:成功返回进程的状态值;当sh不能执行时,返回127;失败返回-1
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("调用system之前!\n");
if(system("ls") == -1){
printf("调用system失败!\n");
perror("why");
}
printf("调用system之后!\n");
return 0;
}
运行结果:
1)简单粗暴,调用直接加可执行程序名,不需要参数。
2)和execl不同的是,调用成功后还会执行下面的语句。
十一、popen函数
FILE *popen(const char *command, const char *type);
参数说明:
command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
mode: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
返回值:
如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断
int pclose (FILE* stream)
参数说明:
stream:popen 返回的文件指针
返回值:
如果调用失败,返回 -1
作用:
popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程这个进程必须由 pclose() 函数关闭。
#include<stdio.h>
#include<stdlib.h>
int main()
{
char retBuf[1024] = {0};
FILE *fp;
fp = popen("pwd","r");
int n_read = fread(retBuf,1,1024,fp);
printf("读到%d个字节 \n读取的内容是:%s\n",n_read,retBuf);
return 0;
>运行结果:
- 所以:比system在应用中的好处,可以获取运行的输出结果。