Linux系统编程4--进程

前话

进程:

程序:死的。只占用磁盘空间。 ——剧本。

进程;活的。运行起来的程序。占用内存、cpu等系统资源。 ——戏。

并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任意时刻都仍只有一个进程在进行

在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时
单核CPU的并发:边听歌、边聊天、边看视频,通过在计算机内存中同时存放相互独立的程序,在管理程序控制之下相互穿插的运行,分时复用CPU,不感觉卡顿是因为转换速度太快了

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行

所有程序都要进入内存当中,上图存储介质中由上到下存储量逐渐变大,存储速度逐渐变慢,其中网络的存储空间可认为是无限的
Write这样一个函数,从硬盘一个程序变成进程通过系统调用进入内存,从内存进入缓存,从缓存进入寄存器(cpu内部),最终放到CPU中运算;CPU中有一个预取器对指令进行预取,再通过译码器对指令进行译码(将二进制译码成CPU语言),译码器再将CPU语言交给ALU,ALU计算完后将计算结果放到寄存器堆中。接着再将cpu处理后的数据放回cache,内存最终给用户

MMU:虚拟内存映射单元

虚拟内存和物理内存映射

linux内存管理(详解) - 知乎 (zhihu.com)

虚拟内存作用:

避免用户直接访问物理内存地址,防止一些破坏性操作,保护操作系统;

每个进程都被分配4GB的虚拟内存,用户程序可使用比实际物理内存更大的地址空间。

当进程需要实际访问内存时候,会由内核的[请求分页机制]产生[缺页异常]调用物理内存页。

MMU作用:

虚拟内存与物理内存的映射

设置修改内存访问级别

借助MMU虚拟内存映射,512M的物理内存运行程序也是4G的地址空间(虚拟地址,不真正存在)

借助MMU做虚拟地址映射:

1、物理内存通过分段和分页机制,将物理内存以page为单位,一个page大小是4KB,物理内存的分配和回收都是基于内存页(page)进行

2、当a.out和b.out中的int a =10在虚拟地址映射到物理地址,存储在不同的物理地址中,大小都是4K
3、当int arr[10000000] 在虚拟地址映射到物理地址,在物理地址中是选取可用地址(内存不连续),在虚拟地址中是连续的

4、a.out和b.out的pcb都在虚拟地址中的内核区域中,其都映射到同一物理内存区域的不同地址中。

此外,CPU存在不同权级,不同的虚拟地址会被分配不同的权级,从而有了之前说的用户访问内核会很慢,其实是CPU的权级在转换。

注:32位操作系统,寄存器大小是4字节

PCB进程控制块

PCB是描述进程的,进程名称、创建时间、进程编号等等

里面包含了:

进程id:系统中每个进程有唯一的id,一个非负整数
            ps aux 返回结果里,第二列是进程id
文件描述符表
进程状态:    初始态、就绪态、运行态、挂起态、终止态。
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息(比如说这里存着虚拟地址真正对应的物理地址)
描述控制终端信息(是否需要终端)
进程工作目录位置
*umask掩码 (进程的概念)(umask默认002)
信号相关信息资源。
用户id和组id

2的32次方为什么是4GB

32位操作系统寻址空间为什么是4GB

加上单位:

2^32byte(B) = 4Gbyte(B)

1、32指的是CPU的32位地址线,可以访问2^32个不同地址

2、CPU有8位位线,即访问可以获得8位数据(1字节(1B))

即:这里的32位表示 2 ^ 32 个地址,32指的是地址线,所以每一个地址线都有 8位位线,也就是1个地址可以访问 8bit 的数据,2 ^ 32 * 8 bit = 2 ^ 32 Byte = 4GB

2^32B = 2^22KB

2^22KB = 2^12MB

2^12MB = 2^2GB = 4GB

fork、getpid、getppid函数

pid_t fork(void)
    创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0.
pid_t getpid(void);获取当前进程 ID
pid_t getppid(void);获取当前进程的父进程 ID

打印父子进程

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

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
    }
    if (pid == 0){
        printf("I am child pid = %d my father pid = %d\n",getpid(),getppid());
    }
    else {
        printf("I am father pid = %d my father pid = %d\n",getpid(),getppid());
    }
    return 0;
}

编译后的结果:

I am father pid = 3179 my father pid = 2773
I am child pid = 3180 my father pid = 3179

循环顺序创建多个子进程

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
  pid_t pid;
  int i;
  for(i=0;i<5;i++)
  {
      pid =fork();
      if(pid==0)
      {
       break;
      }
  }
  if(5==i)//父进程 等待for结束自己台跳出
  {
      sleep(5);
      printf("I am father\n");
  
  }
  else//子进程 提前break跳出 打印
  {
      sleep(i);
      printf("I am %d child \n",i+1);
  }

  return 0;
}

编译后输出:

I am 1 child 
I am 2 child 
I am 3 child 
I am 4 child 
I am 5 child 
I am father

父子进程

为什么需要创建子进程
    每个程序的运行都需要进程,创建子进程可以实现并发
什么时候需要创建子进程
    当父进程执行到某个阶段,接收到某个事件后需要创建一个独立的进程协助其完成任务时
子进程对父进程的继承
    1、为什么要继承父进程的相关资源
    父进程创建子进程是为了其能够协助父进程完成某些操作,因此,父进程必须将其自己的一些资源分享给子进程,以便父子进程共同完成任务。而于此目的不相关的资源,子进程没有必要继承,继承了只会白白浪费内存资源
    2、继承的资源
    用户号UIDs和用户组号GIDs
    环境变量
    堆栈
    共享内存
    打开文件的描述符
    执行时关闭(Close-on-exec)标志
    信号(Signal)控制设定
    nice值,该值表示进程的优先级, 数值越小,优先级越高
    进程调度类别(scheduler class) 
    进程组号
    对话期ID(Session ID) (指:进程所属的对话期 (session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《高级编程》 9.5节
    当前工作目录
    根目录 
    文件方式创建屏蔽字
    资源限制
    控制终端
    3、不继承的资源
    进程号PID
    不同的父进程号(译者注: 即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)
    子进程自己的文件描述符和目录流的拷贝
    异步输入和输出
    文件锁,pending alarms和pending signals
    timer_create函数创建的计时器
    阻塞信号集初始化为空集
    资源使用(resource utilizations)设定为0
    在tms结构中的系统时间
    子进程不继承父进程的进程正文(text), 数据和其它锁定内存(memory locks) (译者注:锁定内存指被锁定的虚拟内存页,锁定后, 不允许内核将其在必要时换出(page out)

其他需要注意:
   1.父进程和子进程拥有独立的地址空间和PID参数

  2.经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈(共享内存)

  3.读时共享、写时复制:就像父进程把自己克隆了一遍。事实上,父进程只复制了自己的PCB块。而代码段,数据段和用户堆栈内存空间并没有复制一份,而是与子进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。由于父进程的PCB和子进程的一样,所以在PCB中断中所记录的父进程占有的资源,也是与子进程共享使用的。这里的“共享”一词意味着“竞争”

父进程对子进程的回收
    1.孤儿进程:父进程先结束的时候,此时子进程变为孤儿进程。系统会把init进程(进程1)变为子进程的父进程
  2.僵尸进程:子进程先于父进程结束,父进程还未来得及将其收尸(系统只是回收了这个进程工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存)
  3.回收函数
    wait:阻塞,子进程结束时,系统向其父进程发送SIGCHILD信号   pid_t wait(int *status);
    waitpid:可以回收指定PID的子进程,可以阻塞式或非阻塞式两种工作模式

         pid_t  waitpid(pid, &status, 0);    //0默认表示阻塞

         pid_t   waitpid(pid,&status,WNOHANG)   //表示非阻塞

总结为:

父子进程相同:

刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式

父子进程不同:

进程id、fork的返回值、各自的父进程、进程创建时间、闹钟、未决信号集

父子进程共享:

读时共享、写时复制。

父子进程之间不共享全局变量

但其共享:

1、文件描述符 2. mmap映射区

注:fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法

exec函数族

fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。

将当前进程的.text、.data 替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程 ID 不变,换核不换壳。

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 execve(const char *path, char *const argv[], char *const envp[]);

wait函数

一个进程终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。

wait函数:    回收子进程退出资源, 阻塞回收任意一个。

    pid_t wait(int *status)

    参数:(传出) 回收进程的状态。

    返回值:成功: 回收进程的pid

        失败: -1, errno

    函数作用1:    阻塞等待子进程退出
    函数作用2:    清理子进程残留在内核的 pcb 资源
    函数作用3:    通过传出参数,得到子进程结束状态

    获取子进程正常终止值:

        WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。

    获取导致子进程异常终止信号:

        WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。

wait获取子进程退出值和异常终止信号

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

int main()
{
    pid_t pid,wpid;
    int status;
    pid =fork();
    if(pid==0)
    {
        printf(" i am child,my id is%d\n",getpid());
        printf("child die\n");
        return 73;
    }
    else if(pid>0)
    {
        wpid=wait(NULL);//不关心怎么结束的
        wpid = wait(&status);//等待子进程结束
        if(wpid==-1)
        {
            perror("wait error");
            exit(1);
        }
        if(WIFEXITED(status))//判断 子进程正常退出判断
        {
            printf("child exit with%d\n",WEXITSTATUS(status));
            printf("------parent  finish\n");
        }
        if(WIFSIGNALED(status))//判断 子进程异常退出判断
        {
            printf("child exit with%d\n",WTERMSIG(status));
        }
    }
    else
    {
        perror("fork");
        return 1;
    }
    
}

waitpid函数

waitpid函数:    指定某一个进程进行回收。可以设置非阻塞。            
waitpid(-1, &status, 0) == wait(&status);

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

    参数:
        pid:指定回收某一个子进程pid

            > 0: 待回收的子进程pid

            -1:任意子进程

            0:同组的子进程。

        status:(传出) 回收进程的状态。

        options:WNOHANG 指定回收方式为,非阻塞。

    返回值:

        > 0 : 表成功回收的子进程 pid

        0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。

        -1: 失败。errno

回收子进程

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/wait.h>  
#include <pthread.h>  
  
  
int main(int argc, char *argv[])  
{  
    int i;  
    pid_t pid, wpid, tmpid;  
  
    for (i = 0; i < 5; i++) {         
        pid = fork();  
        if (pid == 0) {       // 循环期间, 子进程不 fork   
            break;  
        }  
        if (i == 2) {  
            tmpid = pid;  
            printf("--------pid = %d\n", tmpid);  
        }  
    }  
  
    if (5 == i) {       // 父进程, 从 表达式 2 跳出  
        sleep(5);  
  
        //wait(NULL);                           // 一次wait/waitpid函数调用,只能回收一个子进程.  
        //wpid = waitpid(-1, NULL, WNOHANG);    //回收任意子进程,没有结束的子进程,父进程直接返回0   
        //wpid = waitpid(tmpid, NULL, 0);       //指定一个进程回收, 阻塞等待  
        printf("i am parent , before waitpid, pid = %d\n", tmpid);  
  
        wpid = waitpid(tmpid, NULL, WNOHANG);   //指定一个进程回收, 不阻塞  
        //wpid = waitpid(tmpid, NULL, 0);         //指定一个进程回收, 阻塞回收  
        if (wpid == -1) {  
            perror("waitpid error");  
            exit(1);  
        }  
        printf("I'm parent, wait a child finish : %d \n", wpid);  
  
    } else {            // 子进程, 从 break 跳出  
        sleep(i);  
        printf("I'm %dth child, pid= %d\n", i+1, getpid());  
    }  
  
    return 0;  
} 

回收多个子进程

不管是wait还是waitpid一次都只能回收一个子进程,要想回收多个,就是在父进程中加入while循环

// 回收多个子进程  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/wait.h>  
#include <pthread.h>  
  
int main(int argc, char *argv[])  
{  
    int i;  
    pid_t pid, wpid;  
  
    for (i = 0; i < 5; i++) {         
        pid = fork();  
        if (pid == 0) {       // 循环期间, 子进程不 fork   
            break;  
        }  
    }  
  
    if (5 == i) {       // 父进程, 从 表达式 2 跳出  
        /* 
        while ((wpid = waitpid(-1, NULL, 0))) {     // 使用阻塞方式回收子进程 
            printf("wait child %d \n", wpid); 
        } 
        */  
        while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) {     //使用非阻塞方式,回收子进程.  
            if (wpid > 0) {  
                printf("wait child %d \n", wpid);  
            } else if (wpid == 0) {  
                sleep(1);  
                continue;  //跳出本次循环,进入下一次while循环
            }  
        }  
  
    } else {            // 子进程, 从 break 跳出  
        sleep(i);  
        printf("I'm %dth child, pid= %d\n", i+1, getpid());  
    }  
  
    return 0;  
}  

会话和守护进程

会话

进程组:多个进程

会话:多个进程组的集合

1、当父进程创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以组长进程标识:其进程组ID==其进程ID

2、组长进程可以创建一个进程组,创建该进程组中的进程后然后终止。(只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关)

3、进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

4、一个进程可以为自己或子进程设置进程组ID

可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀
创建会话的6点注意事项:

调用进程不能是进程组组长,该进程变成新会话首进程

该进程成为一个新进程组的组长进程

需要root权限(ubuntu不需要)

新会话丢弃原有的控制终端,该会话没有控制终端

该调用进程是组长进程,则出错返回

建立新会话时,先调用fork,父进程终止,子进程调用setsid
1和2表示:调用setsid函数的进程会成为新的会长,同时也是新的组长。即pid、组ID、会话ID三个ID都是同一个
getsid函数:
pid_t getsid(pid_t pid)    获取当前进程的会话id
成功返回调用进程会话ID,失败返回-1,设置error

setsid函数:
pid_t setsid(void)    创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
成功返回调用进程的会话ID,失败返回-1,设置error

守护进程

1、daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
2、不受用户登录注销影响。通常采用以d结尾的命名方式。(httpd、sshd、vsftpd、nfsd)
3、创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader

守护进程创建步骤:

1. fork子进程,让父进程终止。
所有工作在子进程中进行形式上脱离了控制终端

2. 子进程调用 setsid() 创建新会话

3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载

4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422

5. 通常根据需要,关闭/重定向 文件描述符
通常关闭的文件描述符的0、1、2;但是有许多程序员是不关闭文件描述符的(而是重定向)
因为一旦关闭0、1、2,当执行一个函数时,返回的就会是0( 返回文件描述符中可用的最小的那个数),与编程习惯不符
6. 守护进程 业务逻辑。while()

创建一个守护进程:

#include<stdio.h>#include<sys/stat.h>#include<fcntl.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<errno.h>#include<pthread.h>voidsys_err(constchar *str){  
    perror(str);  
    exit(1);  
}  
  
intmain(int argc, char *argv[]){  
    pid_t pid;  
    int ret, fd;  
  
    pid = fork();  
    if (pid > 0)                // 父进程终止  exit(0);  
  
    pid = setsid();           //创建新会话  if (pid == -1)  
        sys_err("setsid error");  
  
    ret = chdir("/home/zhcode/Code/code146");       // 改变工作目录位置  if (ret == -1)  
        sys_err("chdir error");  
  
    umask(0022);            // 改变文件访问权限掩码  close(STDIN_FILENO);    // 关闭文件描述符 0  
  
    fd = open("/dev/null", O_RDWR);  //  fd --> 0  if (fd == -1)  
        sys_err("open error");  
  
    dup2(fd, STDOUT_FILENO); // 重定向 stdout和stderr  dup2(fd, STDERR_FILENO);  
  
    while (1);              // 模拟 守护进程业务.  return0;  
} 

编译运行,结果如下:

编辑

查看进程列表,如下:

编辑

这个daemon进程就不会受到用户登录注销影响。

要想终止,就必须用kill命令 Kill -9 15203杀死daemon进程

Linux中查看进程命令ps aux,ps -ef,ps -A,ps -a_夏已微凉、的博客-CSDN博客
ps aux 查看进程ID
Linux系统中查看父进程ID,进程ID,进程组ID,会话ID的方法:ps ajx_ctrigger的博客-CSDN博客_进程组id怎么看
ps ajx 查看父进程ID,进程ID,进程组ID,会话ID
ps ajx | grep daemon 在注销当前用户再登录之后快速查找daemon
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值