【Linux】系统编程之进程

发布于:2022-11-08 ⋅ 阅读:(650) ⋅ 点赞:(0)

一、进程关键概念(简单了解)

1、什么是程序,什么是进程,有什么区别?

程序是静态的概念,gcc xxx.c –o pro;磁盘中生成pro文件,叫做程序。
进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。

2、如何查看系统中有哪些进程?

a.使用ps指令查看
实际工作中,配合grep来查找程序中是否存在某一个进程,如:ps -aux|grep pro
b.使用top指令查看
类似windows任务管理器

3、什么是进程标识符?

每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证

pid=0:称为交换进程(swapper)
作用—进程调度
pid=1:init进程
作用—系统初始化

编程调用getpid();函数获取自身的进程标识符;getppid();获取父进程的进程标识符

4、什么叫父进程,什么叫子进程?

进程A创建了进程B
那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系

5、C程序的存储空间是如何分配?

在这里插入图片描述
在这里插入图片描述

补充说明强调:
栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
堆:存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
共享库的内存映射区域:这是 Linux动态链接器和其他共享库代码的映射区域。

二、fork创建子进程

1、函数fork

函数原型:

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

返回值:

若成功:子进程返回0,父进程返回子进程ID(非负数)
若失败:返回-1

即,返回0,代表当前进程是子进程;返回非负数,代表当前进程是父进程;返回-1,代表调用失败。

示例1
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        pid_t pid2;
        pid_t retpid;//fork返回值

        pid = getpid();

        printf("before fork: pid = %d\n",pid);

        //pid_t fork(void);
        retpid = fork();

        pid2 = getpid();

        printf("after fork: pid = %d\n",pid2);

        if(pid == pid2){
                printf("this is partent process,retpid = %d,partent pid = %d\n",retpid,getpid());//retpid打印子进程的ID,父进程返回子进程的ID
        }else{
                printf("this is child process,retpid = %d,child pid = %d\n",retpid,getpid());
        }//子进程返回0

        return 0;
}

结果:在这里插入图片描述

fork的两种用法(创建一个子进程的目的):
在这里插入图片描述

示例2:fork实战
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        //pid_t getpid(void);
        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("wait,do nothing!\n");
                }
        }

        return 0;
}

结果:
在这里插入图片描述

2、fork函数创建的子进程与父进程的内存关系

在这里插入图片描述
示例3
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int data = 10;

        printf("partent pid = %d\n",getpid());

        //pid_t fork(void);
        pid = fork();

        if(pid > 0){
                printf("this is partent process,partent pid = %d\n",getpid());
		data = data + 5;
        }else if(pid == 0){
                printf("this is child process,child pid = %d\n",getpid());
                data = data + 100;
        }

        printf("data = %d\n",data);

        return 0;
}

结果:
在这里插入图片描述

父进程跟子进程里的data值是不叠加的,而是各自加各自的,所以我们可以知道fork方式创建的子进程是不跟父进程共用存储空间的。

三、vfork、exit、wait和waitpid

1、函数vfork(函数fork也创建进程,与fork的区别)

关键区别一:
vfork直接使用父进程存储空间,不拷贝。
关键区别二:
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
fork父子进程无规律可循,交替随机运行
在这里插入图片描述

示例1:vfork使用
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;

        //pid_t vfork(void);
        pid = vfork();

        if(pid > 0){
                while(1){
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                }
        }
        return 0;
}

结果:
子进程未调用exit退出,所以只有子进程运行,父进程不执行:
在这里插入图片描述

2、函数exit(进程退出)

函数原型(三种都可以):

#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);

#include <stdlib.h>
void _Exit(int status);

正常退出:
1.main函数调用return
2.进程调用exit(),标准c库
3.进程调用_exit()或者_Exit(),属于系统调用
4.进程最后一个线程返回
5.最后一个线程调用pthread_exit
在这里插入图片描述

异常退出:
1.调用abort
2.当进程收到某些信号时,如Ctrl+C
3.最后一个线程对“取消”(cancellation)请求做出响应
在这里插入图片描述

示例2:exit使用
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;

        pid = fork();

        if(pid > 0){
                while(1){
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                        printf("cnt = %d\n",cnt);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
                        }
                }
        }
        return 0;
}

结果:
在这里插入图片描述
fork创建子进程,父子进程都运行3次,子进程exit退出,父进程未调用wait等待子进程退出,所以子进程变成僵尸进程(后面有介绍):
在这里插入图片描述

3、函数wait和waitpid(等待子进程退出)

(1)函数wait和waitpid介绍

在这里插入图片描述

介绍两个字母符号:
僵尸进程:Z+
正常进程:即有父进程等待的进程,S+

函数原型:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

返回值:

若成功,返回进程ID
若失败,返回0或-1

参数:

status参数(是一个整型数指针):
非空指针:子进程退出状态放在它所指向的地址中
空指针:不关心退出状态

示例3:wait使用(不关心子进程退出状态)
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;

        pid = fork();

        if(pid > 0){
                wait(NULL);
                while(1){
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                        printf("cnt = %d\n",cnt);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
                        }
                }
        }
        return 0;
}

在这里插入图片描述
fork创建子进程,因为父进程调用wait(不关心子进程的退出状态),所以父进程阻塞,子进程先运行3次后调用exit退出,这时父进程为正常进程,正常运行:
在这里插入图片描述

------详细介绍------
1、wait:
在这里插入图片描述
在这里插入图片描述
示例4:wait使用(关心子进程退出状态)
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;
        int status = 10;

        pid = fork();

        if(pid > 0){
                wait(&status);
                printf("child process quit,child status = %d\n",WEXITSTATUS(status));
                while(1){
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                        printf("cnt = %d\n",cnt);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(3);
                        }
                }
        }
        return 0;
}

结果:
在这里插入图片描述
fork创建子进程,因为父进程调用wait(关心子进程的退出状态,利用参数status获取子进程的退出状态),所以父进程阻塞,子进程先运行3次后调用exit退出,退出后返回子进程状态,这时父进程为正常进程,正常运行:
在这里插入图片描述
2、waitpid:
在这里插入图片描述
示例5:waitpid使用(非阻塞版本)
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;
        int status = 10;

        pid = fork();

        if(pid > 0){
                //wait(&status);
                waitpid(pid,&status,WNOHANG);
                printf("child process quit,child status = %d\n",WEXITSTATUS(status));
                while(1){
                        printf("cnt = %d\n",cnt);
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(3);
                        }
                }
        }
        return 0;
}

结果:
在这里插入图片描述
fork创建子进程,因为父进程调用waitpid(非阻塞版本),所以父子进程先随机运行3次,子进程运行3次后调用exit退出,因为waitpid不阻塞,所以退出后返回值为0,这时父进程为正常进程,正常运行,子进程为僵尸进程:
vfork创建子进程,只有子进程运行,运行3次后调用exit退出,父进程未调用wait等待子进程退出,子进程exit后,变成僵尸进程:
在这里插入图片描述

这里的阻塞就是相当于vfork方式创建子进程,父进程处于阻塞状态,让子进程先执行,当父进程收到子进程的退出信号的时候,父进程开始执行。而waitpid不阻塞的机制就有点像fork方式创建的子进程一样,父子进程交替执行,所以这就搞的以waitpid的方式等待有点多余了,所以等待子进程退出还是使用wait函数多,但也有场景需要用waitpid的,所以只是waitpid用的少。

(2)僵尸进程

为什么要等待子进程退出?

父进程等待子进程的退出并收集子进程的退出状态,子进程的退出状态不被收集,子进程就会变成僵死进程(僵尸进程)

示例6
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;

        //pid_t vfork(void);
        pid = vfork();

        if(pid > 0){
                while(1){
                        printf("this is partent process,partent pid = %d\n",getpid());
                        sleep(1);
                        printf("cnt = %d\n",cnt);
                }
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
                        }
                }
        }
        return 0;
}

结果:
在这里插入图片描述
vfork创建子进程,只有子进程运行,运行3次后调用exit退出,父进程未调用wait等待子进程退出,子进程exit后,变成僵尸进程:
在这里插入图片描述

(3)孤儿进程

孤儿进程:父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时的子进程就叫做孤儿进程。
Linux避免系统存在过多的孤儿进程,init进程(进程ID号为1)收留孤儿进程,变成孤儿进程的父进程。

示例7
代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
        //pid_t getpid(void);
        pid_t pid;
        int cnt = 0;
        int status = 10;

        pid = fork();

        if(pid > 0){
                printf("this is partent process,partent pid = %d\n",getpid());
        }else if(pid == 0){
                while(1){
                        printf("this is child process,child pid = %d,my partent pid = %d\n",getpid(),getppid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(3);
                        }
                }
        }
        return 0;
}

结果:
在这里插入图片描述

注意:
pid不是1的原因:
平时在图形界面打开的terminal是伪终端,需要切换界面,命令如下:
图形界面切换到字符型界面:Crtl+Alt+F3/Ctrl+Fn+Alt+F3
字符型界面切换到图形界面:Ctrl+Alt+F2/Ctrl+Fn+Alt+F2
再运行程序,父进程pid变为1成功。

------书本介绍:僵尸和孤儿进程------
在这里插入图片描述

博文推荐:孤儿进程和僵尸进程的概念及进程回收(wait函数及waitpid函数)

四、exec族

作用:
  我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

功能:
  在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

函数原型:

#include <unistd.h>
extern char **environ;

int execl(const char *pathname, const char *arg, ...	/* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...		/* (char  *) NULL */);
int execle(const char *pathname, const char *arg, ...	/*, (char *) NULL, char *const envp[] */);
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[]);

参数介绍:

pathname:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量

返回值:

exec函数族的函数执行
若成功,不会返回
若调用失败,会设置errno并返回-1
然后从原程序的调用点接着往下执行。

1、带l的一类exec函数(l表示list)

包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾。
示例1:execl
代码:

//文件demo18_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");      
    }
    printf("after execl\n");
    return 0;
}
//文件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;
}

结果:
在这里插入图片描述
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径./目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

2、带p的一类exec函数

包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin
示例2:execlp

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    printf("this is pro ps:\n");
    printf("before execlp\n");
    //int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
    if(execlp("ps","ps",NULL) == -1)
    {
        printf("execlp failed!\n");
        perror("error");
    }
    printf("after execlp\n");
    return 0;
}

结果:
在这里插入图片描述
如果用execl执行同样代码:
在这里插入图片描述
从上面的实验结果可以看出,上面的exaclp函数带p,所以能通过环境变量PATH查找到可执行文件ps。

补充:C 库函数 - perror()
C 标准库 - <stdio.h>
描述:
C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误stderr。首先输出字符串str,后跟一个冒号,然后是一个空格。
声明:
下面是perror() 函数的声明。
void perror(const char *str)
参数:
str – 这是 C字符串,包含了一个自定义消息,将显示在原本的错误消息之前。 返回值: 该函数不返回任何值。 在这里插入图片描述

3、带v不带l的一类exec函数

包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL};
示例3:execvp
代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execvp(const char *file, char *const argv[]);

int main(void)
{
    printf("before execvp****\n");
    char *argv[] = {"ps","-l",NULL};
    if(execvp("ps",argv) == -1) 
    {
        printf("execvp failed!\n");     
    }
    printf("after execvp*****\n");
    return 0;
}

结果:
在这里插入图片描述
示例4:execv
代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execv(const char *pathname, char *const argv[]);

int main(void)
{
    printf("this is pro ps:\n");
    printf("before execv\n");
    char *argv[] = {"ps",NULL};

    if(execv("/usr/bin/ps",argv) == -1)
    {
        printf("execv failed!\n");
        perror("error");
    }
    printf("after execv\n");
    return 0;
}

结果:
在这里插入图片描述

4、带e的一类exec函数

尚未实验,博文推荐及参考:linux进程—exec族函数,上面exec族部分也是参考此博客。

示例5:实战exec
与文件学习时修改配置文件的程序相结合。
代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
        //pid_t getpid(void);
        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)
                        {
                                execl("./changeData","changeData","config.txt",NULL);
                                exit(0);
                        }
                }else{
                        printf("wait,do nothing!\n");
                }
        }
        return 0;
}

结果:
在这里插入图片描述
等待用户输入1后,创建子进程,由子进程执行修改配置文件的可执行文件changeData,修改成功如下,LENG=后面的字符被改成字符8。
修改后config.txt文件:
在这里插入图片描述

五、system、popen和pclose

1、函数system

注意:system调用后,程序返回原程序,继续执行
函数原型:

#include <stdlib.h>
int system(const char *command);

参数介绍及返回值:
在这里插入图片描述
示例1:system使用
代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    printf("this is pro ps:\n");
    printf("before system\n");

    if(system("ps") == -1)
    {
        printf("system failed!\n");
        perror("error");
    }
    printf("after system\n");
    return 0;
}

结果:
在这里插入图片描述
这里调用system后,程序返回原程序,继续执行,这里打印了after system,这一点与exec不同,其余功能相同。

2、函数popen和pclose

与system在应用中相比,可以获得运行的输出结果。
函数原型:

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

参数介绍及返回值:
在这里插入图片描述
博文推荐:c语言popen介绍
示例2:popen和pclose使用
代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//FILE *popen(const char *command, const char *type);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

int main(void)
{
    char ret[1024] = {0};
    FILE *fp = NULL;

    fp = popen("ps","r");
    int n_fread = fread(ret,1024,1,fp);

    printf("read ret %d bytes,ret =\n %s",n_fread,ret);
    pclose(fp);
    return 0;
}

结果:
在这里插入图片描述
本文参考:UNIX环境编程(第3版);其余参考及博文推荐在上文中。
最后谢谢阅读,笔者乃小白,如有错误之处还请指正。


网站公告

今日签到

点亮在社区的每一天
去签到