【Linux】多进程基础

文章目录

查看进程相关命令

  • ps -ef: System V 风格查询所有的进程信息,-e 参数表示显示所有进程,-f 表示使用全格式输出,包含更多的列信息。输出是静态的,即显示的是执行命令那一刻的快照。
  • ps aux/ajx:BSD 风格查询所有进程信息其中 aux 是一个快捷方式,等同于 -a -u -x,a 表示所有用户的所有进程,j 添加了作业控制相关信息,而 x 显示没有控制终端的进程,u则显示进程的详细信息。
  • top:实时监视进程信息
  • kill:并非杀死进程,而是给进程发送一个信号
  • kill -l:列出所有信号
  • kill -9:杀死进程 kill -9 进程ID(PID)

进程相关函数

  • 每个进程都由唯一进程号来标识,其类型为pid_t(int整形),当某个进程终止后回收该进程号,可以分配给其他进程使用。

  • 除init进程外,任何进程都是由另一个进程创建的,该进程为父进程,对应的进程号为父进程号(PPID),被创建的进程为其子进程。

  • 进程组是一个或者多个进程的集合,互相关联,进程组可以接收同一终端的信号,关联的进程有一个进程组号(PGID)

    pid_t getpid(void);//获取进程号
    pid_t getppid(void);//获取父进程号
    pid_t getpgid(pid_t pid);//获取进程的组进程号

  • 进程创建函数fork() :父进程调用 fork()后,采用读时共享、写时复制 的创建策略,只为子进程创建新的虚拟地址空间,并复制父进程的虚拟地址空间内容(包括代码段、数据段、堆、栈等)给子进程虽然虚拟地址空间不同,但是共享同一个虚拟地址空间映射(页表),映射到同一物理地址,即不为子进程开辟物理地址空间 。只有在父进程或子进程需要进行写入操作时才会为相应的进程开辟新的物理地址空间 ,从而使各自进程拥有各自的地址空间。子进程也可以创建其新的子进程。fork()会返回两次,通过返回的pid判断是父进程还是子进程,返回值>0为父进程,==0为子进程,交替抢CPU使用。

    #include <sys/types.h>
    #include <iostream>
    #include <unistd.h>
    int main(){
    int num=10;
    //创建子进程
    pid_t pid=fork();

      //如果使用循环创建多个子进程一定注意,只让父进程创建,对于创建的子进程不做处理即需要判断if(pid==0){break;}
    
      //判断父进程还是子进程
      if(pid>0){
          std::cout<<"父进程"<<getpid()<<std::endl;
          num+=10;
          std::cout<<num<<std::endl;
      }
    
      //判断子进程
      else if(pid==0){
          std::cout<<"子进程"<<getpid()<<"父进程"<<getppid()<<std::endl;
          num+=100;
          std::cout<<num<<std::endl;
      }
    
      return 0;
    

    }

  • 进程退出函数

    #include <stdlib.h>//c语言的进程退出,通过调用Linux的进程退出来实现,并且引入了缓冲区
    //同c的文件IO函数fopen和fclose也是引入了缓冲区以及调用Linux的文件IO函数
    void exit(int status)
    //status: 作为进程的退出状态码返回给父进程。通常,0表示正常退出,非零值表示异常终止。

    #include <unistd.h>//Linux的进程退出
    void _exit(int status)

孤儿进程

  • 父进程结束但是子进程还在运行,该子进程就叫孤儿进程,孤儿进程的父进程会被init进程替代,由init进程对其资源进行释放操作,无危害。

僵尸进程

  • 每个进程结束后用户区的数据可以由内核自动释放,而内核区的数据例如PCB需要父进程释放,如果子进程终止,但父进程没有对其内核区的PCB进行回收,子进程的资源残留在内核中,变成僵尸进程,一直占用PID等资源且无法被kill -9杀死,应该及时使用wait()或waitpid()函数进行进程的回收,当子进程结束时会向父进程发送一个SIGCHLD信号,可以使用signal()或者sigaction()对该信号进行捕捉,从而及时的回收子进程的资源,也可以杀死其父进程使其变成孤儿进程最后被init进程回收。

进程回收

  • 父进程通过调用wait()或waitpid()函数进行进程的回收,二者不同的是前者会阻塞等待进程结束,而后者可以设置WNOHANG不阻塞且waitpid()可以指定等待哪个子进程结束,一次调用只能清理一个子进程。

    #include <sys/wait.h>

    pid_t wait(int *status);
    //成功时返回终止的子进程的PID,如果调用进程没有子进程或者信号中断了等待,则返回-1
    //status:一般为NULL,指向一个整型变量的指针,用于接收子进程的退出状态信息。

    pid_t waitpid(pid_t pid, int status, int options);
    //成功时返回终止或停止的子进程PID,如果没有符合条件的子进程,则返回0(如果设置了WNOHANG)或-1(出错)
    //pid:指定等待的子进程PID。有几种特殊情况:
    /

    如果pid为-1,则等待任一子进程。
    如果pid为0,等待同组内的任何子进程。
    如果pid>0,等待指定PID的子进程。
    如果pid<0,等待进程组ID为-pid的所有子进程。
    /
    //status:同wait(),一般为NULL,用于接收子进程的退出状态。
    //options:控制等待行为的标志位,可以是以下选项的组合(通过按位或|操作符):
    /

    WNOHANG:非阻塞等待,如果有子进程已结束则立即返回,没有则立即返回0,不阻塞。
    WUNTRACED:如果子进程停止执行(而不是终止),也报告它的状态。
    WCONTINUED:报告子进程从停止状态变为继续执行的状态。
    */

进程通信(IPC)

  • 管道通信pipe:例如常见的管道符|就是管道通信,ps -ef | grep ssh,将前一个指令的输出作为后一个指令的输入,管道通信是单向的半双工通信,一端读另一端写。管道实际是内存维护的一片缓冲区,可以使用操作文件的方式操作管道 ,使用文件描述符fd以及open()、close()、read()和write()

匿名管道pipe通信的使用

创建管道
#include <unistd.h>
int pipe(int pipefd[2]);
//包括读端pipefd[0]和写端pipefd[1]
//如果成功创建管道,pipe()函数返回0。若出现错误(比如资源不足),则返回-1,并用perror输出。

//创建管道
int pipefd[2];
int ret=pipe(pipefd);
if(ret==-1){
    perror("pipe");
    return 0;
}

//读管道,类似读文件
....
close(pipefd[1]);//关闭写端
char buff[1024]={0};
while(1){
int len=read(pipefd[0],buff,sizeof(buff));
if(len==0){
    break;
}
}
....

//写管道,类似写文件
....
close(pipefd[0]);//关闭读端
const char *str="HELLO!HAHAHA!" ;
write(pipefd[1],str,sizeof(str));
....
  • 匿名管道只能在有公共祖先的进程下使用,因为父进程fork()后,因其写时复制策略,其子进程与父进程的虚拟地址空间的内容一致且共享父进程的虚拟地址空间映射,包括文件描述符表,其中包含读端pipefd[0]和写端pipefd[1],所以才能进行通信。同时因为半双工通信,读进程需要关闭写端close(pipefd[1]),写进程需要关闭读端close(pipefd[0])
  • ulimit -a:用于查看或限制当前用户的各种系统资源限制,可以查看管道缓冲的大小。

有名管道:FIFO

  • 通过创建管道名称(实则是文件作为管道),可以用于无关系之间的进程的通信,也是通过文件描述符和文件IO进行操作。

    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    //pathname:一个指向包含命名管道路径名的字符串指针。
    //mode:指定命名管道的权限位,类似于普通文件的权限设置。
    //如果成功创建命名管道,返回0。如果失败则返回-1,并设置errno变量

    //创建管道test
    int ret=mkfifo("test",0664);
    if(ret==-1){
    perror("管道创建错误");
    return 0;
    }

    //只写打开管道即关闭读端,类似打开文件操作。
    int fd=open("test",O_WRONLY);
    if(fd==-1){
    perror("open");
    return 0;
    }
    //写管道,类似写文件
    ....
    const char *str="HELLO!HAHAHA!";
    write(fd,str,sizeof(str));
    ....

    //只读打开管道即关闭写端,类似打开文件操作。
    int fd=open("test",O_RDONLY);
    if(fd==-1){
    perror("open");
    return 0;
    }
    //读管道,类似读文件
    ....
    char buff[1024]={0};
    while(1){
    int len=read(fd,buff,sizeof(buff));
    if(len==0){
    break;
    }
    }
    ....

读写管道总结

读管道:

  • 管道中有数据则read返回实际读到的字节数,
  • 若管道中无数据:
    1. 若写端全部关闭则read返回0相当于读完
    2. 若写端没有完全关闭则read阻塞

写管道:

  • 管道读端全部被关闭,则进程异常终止
  • 若管道读端没有全部关闭:
    1. 管道已满则write阻塞
    2. 管道未满则write将数据写入并返回实际字节数

mmap内存映射

  • mmap是一种内存映射文件的方法,即将一个文件映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read, write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

  • 函数原型:
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);成功时返回映射区域的起始地址,失败时返回MAP_FAILED

    1. addr:期望映射区域的起始地址,通常设为NULL让系统自动选择合适的地址。
    2. length:映射区域的长度,以字节为单位,使用文件大小即可
    3. prot:映射区域的保护标志,可以是以下值的组合(使用|操作符):
      PROT_READ:可读。
      PROT_WRITE:可写。
      PROT_EXEC:可执行。
      PROT_NONE:不可访问。
    4. flags:映射类型和行为标志,常见组合有:
      MAP_SHARED:映射区域可被多个进程共享,对映射内容的修改会影响原文件或共享内存。
      MAP_PRIVATE:创建映射的私有副本,对映射内容的修改不会影响原文件。
      MAP_ANONYMOUS:用于创建匿名映射,不关联文件。
    5. fd:当映射文件时,此参数为文件描述符;如果是匿名映射,则通常设为-1。
    6. offset:文件中映射区域的偏移量,必须是页对齐的。
  • 具体使用:

    #include <sys/mman.h>
    ....

    //打开文件
    int fd=open("text",O_RDWR);
    int size= lseek(fd,0,SEEK_END);//通过计算偏移量获取文件的大小

    //创建映射
    void *ptr = mmap(NULL,size,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);//返回映射区域起始地址
    if(ptr==MAP_FAILED){
    perror("mmap");
    return 0;
    }

    //实现进程间通信
    pid_t pid = fork();
    if(pid>0){
    //等待子进程写结束
    wait(NULL);
    //父进程,读
    char buff[1024];
    strcpy(buff,(char *)ptr);//读取数据
    }else if(pid==0){
    //子进程,写
    const char *str="HAHAHA!";
    strcpy((char *)ptr,str);
    }

    //关闭映射区
    munmap(ptr,size);

共享内存

  • 在内存中开辟一个共享空间用于进程间的通信,开辟空间后根据内存标识符绑定进程,使用对内存进行操作的函数met-进行相应操作,使用结束后解除关联并对该空间进行删除,只有最后一个解除关联的进程可以对共享内存进行释放 。可以通过指令ipcs -a查看具体的共享内存信息。

    #include <sys/shm.h>

    //创建共享内存
    int shmget(key_t key, size_t size, int shmflg);
    //成功时返回共享内存标识符(一个非负整数,称为shmid)。出错时返回-1,并设置errno
    //key:一个键值,用于标识共享内存段。
    //size:共享内存段的大小,以字节为单位。如果创建新的共享内存段,该参数指定了其大小;如果是连接到已存在的共享内存段,该参数被忽略。
    //shmflg:标志位,用于控制共享内存段的创建和访问权限。主要包含以下几部分:
    /*
    创建标志(IPC_CREAT和IPC_EXCL):
    IPC_CREAT:如果共享内存不存在,则创建一个新的共享内存段。
    IPC_EXCL:与IPC_CREAT一起使用,如果共享内存段已经存在,则调用失败。这可以帮助实现"要么创建新段,要么不操作"的逻辑。
    权限位:类似文件权限,如S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP,定义了谁能读写共享内存段。
    */

    //共享内存关联进程,使用内存标识符
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    //成功时返回指向共享内存起始地址的指针,之后就可以通过这个指针来读写共享内存。出错时返回(void *) -1,并设置errno。
    //shmid:由shmget()函数返回的共享内存标识符。
    //shmaddr:指向希望共享内存段被映射到的进程地址空间中的地址。一般情况下,传递NULL可以让系统自动选择合适的地址。
    //shmflg:标志位,用于控制映射行为,主要包括:
    SHM_RDONLY:以只读模式连接共享内存。如果共享内存段当初创建时没有设置SHM_RDONLY,则此标志无效。

    //删除共享内存
    int shmctl(int shmid, int cmd, struct shmid_ds buf);
    //成功时返回0,出错时返回-1,并设置errno。
    //shmid:由shmget()函数返回的共享内存标识符。
    //cmd:指定要执行的操作,可以是以下几种:
    /

    IPC_RMID:删除共享内存段。
    IPC_STAT:获取共享内存状态,并存入buf指向的结构中。
    IPC_SET:设置共享内存段的某些属性,如权限等,使用buf指定新的属性值。
    */
    //buf:一般为NULL

    //具体使用

    //请求或创建一个大小为1024字节的共享内存段,权限为rw-r--r--
    int shmid = shmget(100, 1024, IPC_CREAT | 0644);
    if (shmid == -1) {
    perror("shmget failed");
    return 0;
    }

    // 将共享内存关联当前进程
    void *shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *) -1) {
    perror("shmat failed");
    return 0;
    }

    //写数据
    const char * str ="helloworld!";
    memcpy(shmaddr,str,sizeof(str));

    //解除关联
    shmdt(shmaddr);

    //删除共享内存
    shmctl(shmid,IPC_RMID,NULL);

    //读端直接通过之前创建的共享内存的key来获取,其他操作同写端

相关推荐
_oP_i几秒前
.NET Core 项目配置到 Jenkins
运维·jenkins·.netcore
weixin_437398218 分钟前
Linux扩展——shell编程
linux·运维·服务器·bash
小燚~10 分钟前
ubuntu开机进入initramfs状态
linux·运维·ubuntu
小林熬夜学编程17 分钟前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
Hacker_Fuchen19 分钟前
天融信网络架构安全实践
网络·安全·架构
炫彩@之星21 分钟前
Windows和Linux安全配置和加固
linux·windows·安全·系统安全配置和加固
上海运维Q先生22 分钟前
面试题整理15----K8s常见的网络插件有哪些
运维·网络·kubernetes
hhhhhhh_hhhhhh_31 分钟前
ubuntu18.04连接不上网络问题
linux·运维·ubuntu
ProtonBase32 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
冷心笑看丽美人39 分钟前
探秘 DNS 服务器:揭开域名解析的神秘面纱
linux·运维·服务器·dns