Linux系统编程 | 【03】进程、环境变量、IPC

一、进程相关概念

1、程序和进程

多道程序⼯作的环境下,操作系统必须能够实现资源的共享程序的并发执⾏,从⽽使程序的执⾏出现了并⾏动态相互制约的新特征。为了能反映程序活动的这些新特点,UNIX 引⼊了进程。UNIX 的进程是⼀个正在执⾏的程序的映象;

  • ⼀个程序是⼀个可执⾏的⽂件,⽽⼀个进程则是⼀个执⾏中的程序实例
  • 在 UNIX/Linux 系统中可以同时执⾏多个进程(多任务设计),对进程数⽬⽆逻辑上的限制,并且系统中可以同时存在⼀个程序的多个实例。各种系统调⽤允许进程创建新进程终⽌进程、对进程执⾏的阶段进⾏同步控制对各种事件的反映。在进程使⽤系统调⽤的条件下,进程便相互独⽴的执⾏了;
  • 程序(静态):编译好的二进制文件;
  • 进程(动态):包括程序代码数据,其中数据包含程序变量数据外部数据和程序堆栈等,占用资源,在内存中执行的程序实例;
  • 同一程序可加载为不同的进程;
2、进程树

在Unix中,⼀个进程⼜可以启动另⼀个进程,这就给 UNIX 的进程环境提供了⼀个类似⽂件系统⽬录树那样的层次结构;

  • 进程树的顶端是⼀个控制进程,它是⼀个名为init的程序的执⾏,该进程是所有⽤户进程的祖先;
3、单道程序设计

一次只能运行一个程序;

4、多道程序设计
  • 同时存放几道相互独立的程序;
  • 首先必须要有支持该硬件的基础;
  • 时钟中断是其基础:强制让进程让出cpu,1秒可执行10亿条指令;
5、CPU和MMU

cpu工作原理

将命令传入到预取器中,后转到译码器进行译码,在传入算数逻辑单元执行,再将处理好的数据传到寄存器堆,最后再传回寄存器;

在这里插入图片描述

存储介质

在这里插入图片描述

MMU工作原理

  • 位于cpu内部;
  • 虚拟内存与物理内存的映射;
  • 设置修改内存访问级别;
6、进程控制块PCB

每个进程在内核中都有一个PCB来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体;

struct task_struct

  • 进程id;
  • 状态;
  • 进程切换时需要保存和恢复的一些cpu寄存器;
  • 描述虚拟地址空间的信息;
  • 描述控制终端的信息;
  • 当前工作目录;
  • umask掩码;
  • 文件描述符表,包含很多指向file结构体的指针;
  • 和信号相关的信息;
  • 用户id和组id;
  • 会话和进程组;
  • 进程可以使用的资源上限;

在这里插入图片描述

7、进程状态

初始态、就绪态、运行态、挂起态、终止态;

二、环境变量

  • 在操作系统中用来指定操作系统运行环境的一些参数;
  • 位置:位于用户区,高于stack的起始位置;
  • 引入环境变量表:需要声明环境变量,extern char ** environ
  • 存储形式:char *[] environ,NULL为结尾;

特征

  • 字符串;
  • 格式:名=值[:值](多个值,则用冒号隔开);
  • 值用来描述进程环境信息;
1、常见环境变量
1.1 PATH
  • 可执行文件的搜索路径。
  • 例:ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名/a.out,由于PATH环境变量的值里面包含了Is命令所在的目录/bin,却不包含a.out所在的目录。PATH 环境变量的值可以包含多个目录,用:号隔开。
  • 在Shell中用echo命令可以查看这个环境变量的值:$ echo $PATH
1.2 SHELL

当前shell:/bin/bash

1.3 TERM
1.4 LANG
1.5 HOME
2、相关函数
2.1 getenv

获取环境变量值

char *getenv(const char *name);
char *secure_getenv(const char *name);

/*
* return: 返回一个指向环境中该值的指针,如果不匹配则返回NULL;
*/
2.2 setenv

设置/添加环境变量的值;

int setenv(char *name, const char *value, int overwrite)

/*
* @param overwrite: 1时,覆盖原环境变量,0时,不覆盖;
* return:成功返回0,失败返回-1;
*/
2.3 unsetenv

删除环境变量name的定义

int unsetenv(const char *name)

/*
* return: 成功返回0,失败返回-1,name不存在仍返回0
*/

三、进程控制

1、fork

通过复制创建一个子进程,但会对性能造成影响,由于该过程只能建立程序副本;

pid_t fork(void);

/*
* return: 失败返回-1,成功返回:父进程返回子进程的ID以及子进程返回0
*/
2、getpid

返回调用进程的进程ID。 (生成唯一临时文件名的例程经常使用此方法)

pid_t getpid(void);

3、getppid

返回调用进程的父进程的进程ID。

pid_t getppid(void);
4、getuid

返回调用进程的真实用户ID。

uid_t getuid(void);

uid_t geteuid(void);
// 获取当前进程有效id
5、getgid

返回调用进程的真实ID。

gid_t getgid(void);
6、进程共享

父子进程间,遵循读时共享写时复制原则;

fork后的相同之处

  • .data
  • .text
  • 栈;
  • 堆;
  • 环境变量;
  • 用户ID;
  • 宿主目录;
  • 进程工作目录;
    信号处理方式;

不同之处

  • 进程ID;
  • fork返回值;
  • 父进程ID;
  • 进程运行时间;
  • 定时器;
  • 未决信号集;

共享

  • 文件描述符(打开文件的结构体);
  • mmap建立的映射区。
7、gdb调试

gdb调试只能跟踪一个进程。可在fork函数调用前,通过gdb调试跟踪进程或其子进程,默认跟踪父进程;

  • set follow-fork-mode child 命令设置gdb在fork之后跟踪父进程;
  • set follow-fork-mode parent 设置跟踪父进程;

三、exec函数

子进程要调用一种exec函数执行另一个程序,并退出该程序;

  • 把⼀个新程序装⼊调⽤进程的内存空间,来改变调⽤进程的执⾏代码,从⽽形成新进程
  • l:命令行参数列表;
  • p:搜索file时使用path变量;
  • v:使用命令参数数组;
  • e:使用环境变量数组;
1、execlp

加载一个进程,借助PATH环境变量;

int execlp(const char *file, const char*arg...);

/*
eg: execl('ls', 'ls', '-l', '-a', NULL);
* @param file: 要加载的程序名。配合PATH来使用,当在PATH中所有目录搜索后无该参数则返回-1;
* return: 只在错误的时候返回-1
*/
2、execl

加载一个进程;与execpl的差别就在于第一个参数;

int execl(const char *path, const char *arg, ...);
/*
* @param path: 路径+程序名;
* return: 成功无返回,失败返回-1;
*/

四、进程终止

1、exit()

该函数⼀个参数 status,称作进程的退出状态,⽗进程可以使⽤它的低 8 位

  • exit()的返回值通常⽤于指出进程所完成任务的成败。如果成功,则返回 0;如果出错,则返回⾮ 0 值;
  • exit()除了停⽌进程的运⾏外,能够将关闭所有已打开的⽂件。如果⽗进程因执⾏了 wait⽽处于睡眠,那么⼦进程执⾏exit()会重新启动⽗进程运⾏。且,exit()还将完成⼀些系统内部的清除⼯作,如缓冲区的清除⼯作等;
  • 除了使⽤ exit()来终⽌进程外,当进程运⾏完其程序到达main()函数末时,进程会⾃动终⽌。当进程在 main()函数内执⾏⼀个return时,也会终⽌。

五、回收子进程

1、孤儿进程

父进程先于子进程结束,则子进程为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程;

2、僵尸进程
  • 进程终止,父进程尚未收回,子进程残留PCB在内核中;
  • kill不能终止僵尸进程,因为僵尸进程已终止。
3、wait函数

父进程可调用wait获取,在清除该进程。

  • 阻塞等待子进程退出;
  • 回收子进程残留资源;
  • 获取子进程结束状态(退出原因)。
pid_t wait(int *status);


/*
* @param status: 为传出参数,子进程状态;
* 	- WIFEXITED为(status)非0 - 进程正常结束;
* 	- WEXITSTATUS(status) - 上一个宏为真,则获取进程退出状态,exit的参数;
* 	- WIFSIGNALED(status) - 进程异常终止;
* 	- WTERMSIG(status) - 上一个宏为真,则取得使进程终止的那个信号的编号;
* return: 返回终止子进程的进程ID; 出错时,返回-1;
*/
4、waitpid函数

作用与wait相同,指定pid进程清理,可不阻塞;

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

/*
* @param pid: 
* 	>0 - 回收指定ID的子进程;
* 	-1 - 回收任意子进程(=wait);
* 	 0 - 回收和当前调用waitpid一个组的所有子进程;
*  <-1 -回收指定进程组内的任意子进程;
* return: 成功返回清理的子进程,失败返回-1(无子进程),返回0参数3为WONHANG,且子进程正在运行;
*/
5、案例
#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

int main(){
    pid_t pid, wpid;
    int status;
    //for(int i=0; i<5; i++)
    pid = fork();

    if(pid == 0){
        // 子进程, getppid获取父进程ID
        std::cout << "我是子进程,父进程为:" << getppid() << std::endl; 
        //sleep(20);
        exit(1);
    }else if(pid == -1){
        // 失败
        std::cout << "error" << std::endl;
    }else{
        wpid = wait(&status);
        // 异常退出
        if(WIFSIGNALED(status) != 0){
            std::cout << "子进程的异常退出状态:" << WTERMSIG(status) << std::endl;
        } 
        // 正常退出
        if(WIFEXITED(status)){
            std::cout << "子进程的正常退出状态:" << WEXITSTATUS(status) << std::endl;
        }
        while(1){
            std::cout << "父进程ID:" << getpid() << "子进程为L: "<< pid << std::endl;
            sleep(1);
        }
    }
    return 0;
}

六、IPC方法

Linux下进程是相互独立的,不能相互访问。若要交换数据则需要通过内核,在内核开辟一块缓冲区,将进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,该机制为IPC进程间通信

常用的通信方式

  • 管道 - 最简单;
  • 信号 - 开销最小;
  • 共享映射区 - 无血缘关系;
  • 本地套接字 - 最稳定。
1、管道
  • 其本质是一个伪文件(实为内核缓冲区4k);
  • 由俩个文件描述符引用,一个读端,一个写端
  • 规定数据从管道的写端流入管道,从读端流出

局限性

  • 数据自己读不能自己写
  • 数据被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通信方式。因此,数据只能再一个方向上流动;
  • 只能再有公共祖先的进程间使用管道;

常用通信方式

  • 单工通信:只能发生信号 - 遥控器;
  • 半双工通信:单向发生或接收 - 微信;
  • 全双工通信:双向发生接收 - 通话;
1.1 pipe

创建一个管道;

int pipe(int pipefd[2]);

/*
* @param pipefd:
* 	pipefd[0]指的是管道的读端
* 	pipefd[1]指到管道的写入端
* return: 成功返回0,失败返回-1
*/
1.2 案例
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<cstring>
/*
 *  1、创建子进程,管道
 *  2、将子进程设为读、父进程设为写
 *  3、通过管道实现进程间通信
 * */
int main(int argc, char *argv[]){
    pid_t pid;
    int fd[2];

    int ret = pipe(fd);
    if(ret == -1){
        std::cout << "pipe create error..." << std::endl;
        exit(1);
    }

    pid = fork();
    if(pid == -1){
        std::cout << "child process error..." << std::endl;
        exit(1);
    }else if(pid == 0){ // 子进程
        std::cout << "child process id: " << getpid() << std::endl;
        close(fd[1]); // 关闭写操作
        char buf[1024];
        ret = read(fd[0], buf, 1024);
        if(ret == -1){
            std::cout << "file read error..." << std::endl; 
            exit(1);
        }
        // 将读到的数据输出
        write(STDOUT_FILENO, buf, ret);
    }else{ // 父进程
        close(fd[0]); // 关闭读操作
        std::string myStr = "test pipe\n";
        write(fd[1], myStr.c_str(), myStr.size());
    }

    return 0;
}

2、共享存储映射
2.1存储映射I/O
  • 使一个磁盘文件与存储空间中的一个缓冲区相映射(相当于从缓冲区取数据)。类似于,将数据存入缓冲区,则相应字节就自动写入文件,即使用I/O地址操作;
  • 首先应通知内核,将一个指定文件映射到存储区域,可通过mmap函数实现;
2.2 mmap
void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

/*
* @param addr: 建立映射区的首地址,有Linux内核指定,使用时,传入NULL即可;
* @param length: 指定映射的长度;
* @param prot:映射区权限;
* 	- PROT_NONE: 
* 	- PROT_EXEC: 可能被执行;
* 	- PROT_READ: 读取;
* 	- PROT_WRITE: 写;
* 	- PROT_NONE: 无法访问;
* @param flags: 标志位参数(用于设定更新物理区域、设置共享、创建匿名映射区)
* 	- MAP_SHARED:会将映射区所做的操作反映到物理设备上;
* 	- MAP_PRIVATE:映射区所做的修改不会反映到物理设备;
* @param fd:用来建设映射区的文件描述符;
* @param offset:映射文件的偏移(4k的整数倍);
* return: 返回一个指向映射区域的指针,失败返回MAP_FAILED;
*/
2.3 munmap

系统调用删除指定地址范围的映射,并导致对在产生无效内存引用的范围内的地址。 该区域也会自动取消映射进程终止。

int munmap(void *addr, size_t length);

/*
* @param addr: map的返回值;
* return: 失败返回0;
*/
2.4 常见问题
  • 映射区大小不能为0;
  • 创建映射区的权限要 <= 文件打开的权限,映射区的创建隐含着对文件的读操作;
  • offset必须是4k的整数倍;
  • 映射区的释放与文件关闭无关,只要映射建立成功,文件即可关闭
  • munmap传入的第一个参数一定是mmap返回的地址;
2.5 案例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>

/*
 * 1、打开文件,并获取文件大小
 * 2、创建映射区
 * 3、
 * */
int main(int argc, char *argv[]){

    char *p = NULL;
    int fd = open("test.txt", O_CREAT|O_RDWR, 0644); // 打开/创建文件
    if(fd < 0){
        std::cout << "open error..." << std::endl;
        exit(1);
    }

    // 拓展文件大小
    int ret = ftruncate(fd, 4);
    if(ret == -1){
        std::cout << "ftruncate error..." << std::endl;
        exit(1);
    }
    // 创建映射区,注意c++中需要强转
    p = (char *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED){
        std::cout << "mmap error..." << std::endl;
        exit(1);
    }

    std::cout << "-----------------------" << std::endl;
    strcpy(p, "test\n");
    ret = munmap(p, 4);
    if(ret == -1){
        std::cout << "munmap error..." << std::endl;
        exit(1);
    }
    std::cout << "over............." << std::endl;
    close(fd);

    return 0;
}

2.6 mmap父子进程通信

血缘关系的进程可通过mmap建立的映射区来完成数据通信,需要设置对应的flag:

  • MAP_PRIVATE:(私有映射)父子进程各自独占映射区;
  • MAP_SHARED:(共享映射)父子进程共享映射区;
  • 结论
    • 打开的文件;
    • mmap建立的映射区,需要用MAP_SHAPED

案例

#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/mman.h>

int t_val = 1; 

/*
*结论:
*    打开的文件;
*    mmap建立的映射区,需要用`MAP_SHAPED`
*/
int main(int argc, char *argv[]){

    pid_t pid; // 存储fork返回的子进程ID
    
    int fd = open("temp.txt", O_RDWR|O_CREAT|O_TRUNC, 0644);
    if(fd < 0){
        perror("open error");
        exit(1);
    }
    /*
     *  删除临时文件目录项,让所有占用该文件的进程都结束后被删除
     * */
    unlink("temp.txt"); 
    ftruncate(fd, 4);

    int *p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

    if(p == MAP_FAILED){
        perror("mmap error");
        exit(1);
    }

    close(fd); // 映射区建立好即可关闭

    pid = fork();
    if(pid == 0){
        // 子进程返回
        *p = 1000;
        t_val = 200;
        std::cout << "child: " << getpid() << "*p: " << *p << "t_val: " << t_val << std::endl;
    }else if(pid < 0){
        perror("fork error");
        exit(1);
    }else{
        sleep(1);
        std::cout << "parent: " << getppid() << "  child: " << pid <<  "  *p: " << *p << "  t_val: " << t_val << std::endl;
        wait(NULL);
        
        int ret = munmap(p, 4);
        if(ret == -1){
            perror("munmap error");
            exit(1);
        }
    } 
    std::cout << "---------------" << std::endl;
    return 0;
}

2.7 匿名映射

无需依赖一个文件即可创建映射区,且需要标志位来flag来指定;

  • flag只需或上:MAP_ANONYMOUS/MAP_ANON
  • 文件描述符使用-1代替;
  • 是linux系统特有的;

类UNIX系统中

  • 需要借助 fd = open("/dev/zero", O_RDWR)
  • 该文件无大小;
2.8 mmap无血缘关系进程间通信

写文件

#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/mman.h>
#include<cstring>
#include<stdio.h>


/*@ 学生结构体 */
typedef struct STU{
    int id;
    char name[10];
    char sex;
}stu;

/* 写文件
 * 
 * */
int main(int argc, char *argv[]){
    
    STU student = {10, "xiaoming", 'm'};
    char *mm;
    
    if(argc < 2){
        std::cout << "./a.out file_shared" << std::endl;
        exit(-1);
    }
    // 打开文件
    int fd = open(argv[1], O_RDWR|O_CREAT, 0644);
    ftruncate(fd, sizeof(student));

    // 建立映射区
    mm = (char *)mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(mm == MAP_FAILED){
        perror("mmap error");
        exit(-1);
    }
    
    close(fd);

    while(1){
        memcpy(mm, &student, sizeof(student));
        student.id++;
        std::cout << "write..." << std::endl;
        sleep(1);
    }

    munmap(mm, sizeof(student));
    return 0;
}

读文件

#include<iostream>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<cstring>
#include<sys/mman.h>
#include<stdio.h>
/*@ 学生结构体  */
typedef struct stu{
    int id;
    char name[10];
    char sex;
}STU;


/* 读文件
 * 
 * */
int main(int argc, char *argv[]){
    
    STU student;
    STU *st;

    if(argc < 2){
        std::cout << "a.out file_sharead\n" << std::endl;
        exit(-1);
    }

    // 只读模式,打开文件
    int fd = open(argv[1], O_RDONLY);
    if(fd == -1){
        perror("open error");
        exit(-1);
    }

    // 建立映射,建立通信
    st = (STU *)mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
    if(st == MAP_FAILED){
        perror("mmap error");
        exit(-1);
    }
    close(fd);
    
    // 打印数据
    while(1){
        std::cout << "id: " << st->id << "name: " << st->name << "性别: " << st->sex << std::endl;
        sleep(2);
    }
    // 终止映射
    munmap(st, sizeof(student));

    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值