Linux之进程

备用知识:

 并发:指的是多个任务在宏观上同时进行,但在微观上可能是交替执行的。并发可以发生在单核处理器上,通过时间分片或多任务处理来实现。操作系统会将CPU时间分配给不同的任务,使得这些任务看起来像是同时进行的,尽管实际上它们是交替执行的。并发的关键在于任务之间的切换足够快,以至于用户感觉它们是同时运行的。

并行:指的是多个任务或操作在物理上同时进行。这通常需要多个处理器或核心来实现。在并行计算中,每个处理器或核心可以独立地执行不同的任务或任务的一部分。并行计算可以显著提高处理速度,特别是在处理可以分解为多个独立部分的复杂问题时。

PCB:存放一个具体进程的全部信息(包括进程编号、状态、优先级等),因为系统有多个进程,所以就存在多个PCB,每个PCB之间通过指针变量关联

进程

正在内存中运行的程序被称为进程,进程是系统分配资源的最小单元。   

 进程的创建

除了系统的初始化进程之外,其他的所有进程都是通过 fork() 复刻而来的。调用 fork()函数的进程称为父进程,由fork()函数创建出来的进程被称为子进程

#include <unistd.h>
pid_t fork(void);   //在父进程中返回子进程PID号

 一个进程复刻一个子进程的时候,会将自身几乎所有的资源复制一份,比如整个内存空间,包括栈、堆、数据段、代码段、标准IO的缓冲区等等。但是如下属性不会复制:

  • 进程号PID。PID是身份证号码,哪怕亲如父子,也要区分开。
  • 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
  • 挂起的信号。这是所谓“悬而未决”的信号,等待着进程的响应,子进程不会继承这些信号。

注意:子进程会获得父进程所有文件描述符的副本,这也意味着父、子进程对应的文件描述符均指向相同的文件,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。

fork()函数用途:父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。

进程号

Linux 系统下的每一个进程都有一个进程号(process ID,简称 PID),用于唯一标识系统中的某一个进程。

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);      //获取本进程号
pid_t getppid(void);     //获取父进程号

进程的状态

就绪态 : 指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU 就能够直接运行。

运行态: 指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态。

僵尸态: 僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”。

等待态(或者叫阻塞态):表示进程处于一 种等待状态,等待某种条件成立之后便会进入到就绪态。

  • 浅度睡眠:表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒
  • 深度睡眠:深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。

暂停态: 暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP 信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。

僵尸进程和孤儿进程

当一个进程创建子进程之后,它们俩就成为父子进程关系,父进程与子进程的生命周期往往是不相同 的,这里就会出现两个问题:

孤儿进程

父进程先于子进程结束(子进程变成孤儿)。在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(内核启动,进程号为1)的子进程。

如果使用ubuntu,孤儿的父进程可能因为封装的缘由,导致父进程号不为1。

僵尸进程

子进程先于父进程结束(且父进程未回收子进程时,子进程变成僵尸)。

进程的回收

在很多应用程序的设计中,父进程需要知道子进程何时终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视,同时还需要回收僵尸进程的资源。

wait()

系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
//status: 参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
//返回值: 若成功则返回终止的子进程对应的进程号;失败则返回-1。

  • 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;
  • 如果进程调用 wait(),但是该进程并没有子进程, 也就意味着该进程并没有需要等待的子进程,那么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
  • 如果当前进程有不止1个子进程,则该函数回收第一个变成僵尸态的子进程的系统资源(内核删除僵尸进程)。

参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status 参数:

如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(), 故而从系统中移除僵尸进程。

使用 wait()系统调用存在着一些限制,这些限制包括如下:

  • 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等 待下一个子进程的终止,一个一个来、谁先终止就先处理谁
  • 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,通过判断即可得知是否有子进程终止。

waitpid()

非阻塞等待

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

进程的未来

当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序

exec族函数

系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执 行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
//filename: 参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
//argv: 参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组, 该数组对应于 main(int argc,char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
//envp: 参数 envp 也是一个字符串指针数组, 指定了新程序的环境变量列表, 参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。
//返回值: execve 调用成功将不会返回;失败将返回-1,并设置 errno。

而exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀,这些库函数 都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同, 包括: execl()、 execlp()、 execle()、 execv()、 execvp()、 execvpe()。后缀字母的含义:

  • l : list 以列表的方式来组织指定程序的参数
  • v: vector 矢量、数组,以数组的方式来组织指定程序的参数
  • e: environment 环境变量,执行指定程序前顺便设置环境变量
  • p: 专指PATH环境变量,这意味着执行程序时可自动搜索环境变量PATH的路径
// father.c
#include <stdio.h> 
#include <unistd.h> 
#include <sys/wait.h> 

int main()
{
    // 子进程
    if(fork() == 0)
    {
        printf("加载新程序之前的代码\n");
        // 加载新程序,并传递参数3
        execl("./child", "./child", "3", NULL);
        printf("加载新程序之后的代码\n");
    }
    // 父进程
    else
    {
        // 等待子进程的退出
        int status;
        int ret = waitpid(-1, &status, 0);
        if(ret > 0)
        {
            if(WIFEXITED(status))
                printf("[%d]: 子进程[%d]的退出值是:%d\n",getpid(), ret, WEXITSTATUS(status));
        }
        else
        {
            printf("暂无僵尸子进程\n");
        }
    }
}

 VS code远程连接ubuntu编译结果:

 子进程中加载新程序之后的代码无法运行,因为已经被覆盖了。

进程的守护

也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性 地执行某种任务,主要表现为以下两个特点:

  • 长期运行:一般在系统启动时开始运行,除非强行终 止,否则直到系统关机都会保持运行,与守护进程相比,普通进程都是在用户登录或运行程序时创 建,在运行结束或用户注销时终止
  • 与控制终端脱离:在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,当终端关闭时,由终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行。

进程间通讯

进程间通信(简称 IPC) 。 IPC的方式通常有

  • UNIX IPC:管道、具名管道、信号
  • System V IPC:信号量、消息队列、共享内存
  • Socket IPC:套接字

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),该控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。

管道

把一个进程连接到另一个进程的数据流称为管道,分为有名管道和无名管道两种,它们的区别是:

无名管道

无名管道适用于一对一的、具有亲缘关系的进程间的通信。

#include <unistd.h>
int pipe( int fd[2] );
  • 创建 :pipe函数用来创建无名管道
  • 操作 :read读;write写
  • 关闭操作端口 :close

具名管道(FIFO)  

有名管道又称为命名管道,可以在任意两个进程之间进行通信。它是对无名管道的一种改进,其更接近普通文件,有文件名。

特性:

  • 它可以使互不相关的两个进程实现彼此通信
  • 该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立管道之后,两个进程就可以把它当作普通文件进行读写,使用非常方便。
  • FIFO严格遵循先进先出原则,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。有名管道不支持如Iseek()等文件的定位操作。

操作:

  • 创建有名管道文件 :mkfifo即是命令也是函数;mknod也可以创建管道文件
  • 打开有名管道 :open
  • 读/写 :read/write
  • 关闭 :close
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//pathname:具名管道的名称
//mode:文件权限模式,例如0666

具名管道一旦没有任何读者和写者,系统判定管道处于空闲状态,会释放管道中的所有数据 

System V IPC 

包括消息队列、共享内存、信号量,V 是 Unix 的AT&T 分支的其中一个版本。一般习惯称呼他们为IPC对象,操作接口都比较类似,在系统中使用key 的键值作为唯一标识,而且他们都是“持续性”资源--即他们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命令删除他们。

共同的特性:

  • 在系统中使用所谓键值(KEY)来唯一确定,类似于文件系统中的文件路径
  • 当某个进程创建(或打开)一个IPC对象时,将会获得一个整型ID,类似于文件描述符
  • IPC对象属于系统,而不是进程,因此在没有明确删除操作的情况下,IPC对象不会因为进程的退出而消失
ipcs -q/m/s/a    //查看IPC对象

ipcrm -Q key     // 删除指定的消息队列
ipcrm -q id      //删除指定的消息队列

ipcrm -M key     // 删除指定的共享内存
ipcrm -m id:    //删除指定的共享内存

ipcrm -S key     // 删除指定的信号量
ipcrm -s id      //删除指定的信号量
// 以当前目录和序号1为系数产生一个对应的键值
    key_t key = ftok(".", 1);

消息队列

        可以认为是一个消息列表。提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的消息,进程只要指定这个标识就可以读取数据,而不受干扰。从运行效果来看,一个带标识的消息队列,就像多条并存的管道一样。

        消息队列实现了对消息发送方和消息接收方的解耦,一个进程在往一个消息队列中写入消息之前,不需要有某个进程在该队列上等待消息到达。使得双方可以异步处理消息数据,这一特点对分布式环境特别有用。

原理:

1.发送者:

  • 获取消息队列的 ID
  • 将数据放入一个附带有标识的特殊的结构体,发送给消息队列。

2.接收者:

  • 获取消息队列的 ID
  • 将指定标识的消息读出。

当发送者和接收者都不再使用消息队列时,及时删除它以释放系统资源。

创建

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

key:键值,全局唯一标识,可由ftok()产生
msgflg:操作模式与读写权限:
        IPC_CREAT:用来创建一个消息队列
        IPC_EXCL:查询由key指定的消息队列释放存在
        IPC_NOWAIT:之后的消息队列操作都为非阻塞
        与文件操作函数open类似,同时也可以指定权限
返回值:消息队列MSG对象ID

例如:
    
    // 创建(若存在则报错)key对应的MSG对象
    int msgid = msgget(key, IPC_CREAT|IPC_EXCL|0666);

发送消息的函数

接收消息的函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgqid:MSG对象的ID,由msgget()获取
msgp:存放消息的内存入口
msgsz:存放消息的内存大小
msgtyp:欲接收消息的类型
        0:不区分类型,直接读取MSG中的第一个消息
        大于0:读取类型为指定msgtyp的第一个消息
        小于0:读取类型小于等于msgtyp绝对值的第一个具有最小类型的消息。例如当MSG对象中有类型为                3、1、5类型消息若干条,当msgtyp为-3时,类型为1的第一个消息将被读取
msgflg:接收选项
        0:默认接收模式,在MSG中无指定类型消息时阻塞
        IPC_NOWAIT:非阻塞接收模式,在MSG中无指定类型消息时直接退出函数并设置错误码为ENOMSG
        MSG_EXCEPT:读取除msgtyp之外的第一个消息
        MSG_NOERROR:如果待读取的消息尺寸比msgsz大,直接切割消息并返回msgsz部分,读不下的部分直接丢弃。若没有设置该项,则函数将出错返回并设置错误码为E2BIG

删除

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msqid:MSG对象ID
cmd:控制命令字
        IPC_STAT:获取该MSG的信息,储存在结构体msqid_ds中
        IPC_SET:设置该MSG的信息,储存在结构体msqid_ds
        IPC_RMID:立即删除该MSG,并且唤醒所有阻塞在该MSG上的进程,同时忽略第三个参数

共享内存

共享内存是通过不同进程共享一段相同的内存来达到通信的目的,共享内存是众多IPC方式最高效的一种方式,一般情况下共享内存是不能单独使用的,需要配合诸如互斥锁、信号量等协同机制使用。

步骤:

  1. 创建ipc系统唯一标识key -> ftok
  2. 创建共享内存 -> shmget
  3. 映射共享内存 -> shmat
  4. 共享内存读写
  5. 解除共享内存映射 -> shmdt
  6. 删除共享内存 -> shmctl

创建

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

key:SHM对象键值
size:共享内存大小
shmflg:创建模式和权限
        IPC_CREAT:如果key对应的共享内存不存在,则创建SHM对象
        IPC_EXCL:如果该key对应的共享内存已存在,则报错
        权限与文件创建open类似,用八进制表示
        SHM HUGETLB
返回值:SHM对象ID

获取共享内存地址

#include <sys/types.h>
#include <sys/shm.h>
void *shmat (int shmid, const void *shmaddr, int shmflg);

shmid:指定的共享内存的ID
shmaddr:指定映射后的地址,因为是虚拟地址,分配的原则要兼顾诸如段对齐、权限分配等问题,因此用户进程是无法指定的,只能由系统自动分配,因此此参数一般为NULL,表示交由系统来自动分配
shmflg:可选项
        0:默认,代表共享内存可读可写
        SHM_RDONLY:代表共享内存只读
返回值:共享内存映射后的虚拟地址入口

解除映射

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

int shmdt(const void *shmaddr);

 共享内存控制函数

#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid ds *buf);

shmid:指定的共享内存的ID
cmd:一些命令字
        IPC_STAT:获取共享内存的一些信息,放入shmid_ds{ }中
        IPC_SET:将 buf 中指定的信息,设置到本共享内存中
        IPC_RMID:删除指定的共享内存,此时第三个参数 buf 将被忽略
buf:用来存放共享内存信息的结构体

信号量 

信号量SEM,不是用来传输数据的,而是用来协调进程或者线程工作的,像是一种旗语。

扩充知识:Linux 中用到的信号量有3种:ststem-V 信号量、POSIX有名信号量和 POSIX无名信号量。他们虽然有很多显著不同的地方,但是最基本的功能是一致的:用来表征一种资源的数量,当多个进程或者线程争夺这些稀缺资源的时候,信号量用来保证他们合理地、秩序地使用这些资源,而不会陷入逻辑谬误之中。信号量机制是一种有效的进程同步和互斥工具。

P/V操作

  • 临界资源:多个进程或线程有可能同时访问的资源
  • 临界区:访问这些资源的代码称为临界代码,这些代码区域称为临界区
  • P操作:程序进入临界区之前必须对资源进行申请,这个动作称为P操作
  • V操作:程序离开临界区之后必须释放相应的资源,这个动作称为V操作

进程的互斥:指当一个进程进入临界区使用临界资源时,需要使用临界资源的其他进程必须等待。退出临界区后,需要使用该临界资源的进程解除阻塞。互斥是进程之间的间接制约关系

进程的同步:进程同步是指为完成某种任务而建立的两个及两个以上的进程在某些位置上因工作次序的需要而等待、传递信息所产生的直接制约关系,简单来说就是进程必须严格按照某种先后次序来运行

申请信号量

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

参数:
key:    信号量的key值    
nsems:   信号量元素的个数。例如: 时间+空间  -> 2
semflg:  IPC_CREAT|0666  -> 不存在则创建

返回值:
    成功: 信号量ID
    失败: -1

控制/设置 信号量值参数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

semid:信号量ID
semnum:需要操作的成员的下标   时间:0  空间:1
cmd:    
    SETVAL  -> 用于设置信号量的起始值
    IPC_RMID -> 删除信号量的ID
...:    空间/数据的起始值
     例如: 想设置空间的起始值为1,数据的起始值为0
            semctl(semid,0,SETVAL,1);
            semctl(semid,1,SETVAL,0);
返回值:
        成功:0
        失败:-1

如何实现信号量的P/V操作? (P操作: 1->0 V操作: 0->1)

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);

函数参数:
        semid:  信号量ID号
        sops:进行P/V操作结构体
        nsops: 信号量操作结构体的个数 -> 1

结构体
struct sembuf{
unsigned short sem_num;   //需要操作的成员的下标  时间:0  空间:1
short          sem_op;    //P操作/V操作           P: -1  V: 1
short          sem_flg;   //普通属性,填0.
}

返回值:
        成功:0
        失败:-1

套接字

套接字(Socket)是一种进程间通信(IPC)机制,它在网络编程中非常常用。套接字允许运行在不同主机上的进程通过网络进行通信。

套接字通信模型非常灵活,支持多种通信协议和类型,能够适应不同的网络环境和应用需求。通过套接字,进程不仅可以在同一台主机上通信,还可以跨越网络与远程主机上的进程通信。

知识浅薄、未完待续

  • 31
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值