进程和进程通信(一)

Posted by Distiny on May 5, 2022

进程(process)概念

进程——执行的程序

  • 具有一个程序计数器(用于表示下个执行命令)和一组相关资源
  • 包括进程堆栈stack(临时数据)和数据段data section(全局变量),有的还可能包含堆heap(动态分配内存)

进程状态

​ ————(中断)————

​ ↓ ↑

new—(成功)—>ready——(进行调度)——>run——(退出)——>terminated

​ 等待结束↑ ↓ I/O或事件的等待

​ ————wait<————

进程控制块(PCB)

用于保存进程的相关信息,用于在进程切换时保存状态和重新加载

相关信息:进程状态,程序计数器,CPU寄存器,CPU调度信息,内存管理信息,记账信息,I/O状态信息

线程

进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源

即一个进程可以并行运行多个线程

进程调度

作业队列——就绪队列——设备队列等等

OS通过调整PCB所在的队列来控制进程有序执行

调度器

  • 长期调度程序(作业调度程序) 磁盘(存储设备)的缓冲池——>内存

  • 短期调度程序(CPU调度程序) 准备执行的进程队列——>CPU

上下文切换

切换CPU到另一个进程需要保存当前进程状态和恢复另一个进程的状态

进程运行

创建

pid:进程的唯一标识符

进程树:Linux系统从根进程init开始,逐步创建子进程,形成进程树

ps -el #查看进程列表

F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
4 S     0       1       0  0  80   0 - 41860 -      ?        00:00:02 systemd
1 S     0       2       0  0  80   0 -     0 -      ?        00:00:00 kthreadd
1 I     0       3       2  0  60 -20 -     0 -      ?        00:00:00 rcu_gp
1 I     0       4       2  0  60 -20 -     0 -      ?        00:00:00 rcu_par_gp
1 I     0       6       2  0  60 -20 -     0 -      ?        00:00:00 kworker/0:
1 I     0       9       2  0  60 -20 -     0 -      ?        00:00:00 mm_percpu_
1 S     0      10       2  0  80   0 -     0 -      ?        00:00:00 rcu_tasks_
1 S     0      11       2  0  80   0 -     0 -      ?        00:00:00 rcu_tasks_
1 S     0      12       2  0  80   0 -     0 -      ?        00:00:00 ksoftirqd/
1 I     0      13       2  0  80   0 -     0 -      ?        00:00:04 rcu_sched

部分结果如上图,进程1,2的父进程为0(init),其余进程的父进程为进程2(kthreadd)

进程创建子进程时,有两种执行可能:

  • 父子进程并发执行
  • 父进程等待子进程结束

新进程地址空间有两种可能:

  • 父进程的复制品(与父进程同样的程序和数据)
  • 加载新程序

fork()可以用来创建新进程(复制原来进程地址空间)

​ 返回值——子进程返回0,父进程返回子进程的pid(非0)

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

int main()
{
pid_t pid;
int status;
int a;
pid = fork();

if(pid<0){		//创建进程失败
  fprintf(stderr, "Fork Failed");
  return 1;
}
else if(pid==0){	//进入子进程
  execlp("/bin/ls","ls",NULL);	//执行命令ls
    							//子进程代码结束,自动exit()
}
else {	//父进程
  /*wait函数介绍
  	函数原型:extern __pid_t wait (int *__stat_loc);
  	Wait for a child to die.  When one does, put its status in *STAT_LOC
   	and return its process ID.  For errors, return (pid_t) -1.
  */
  a=wait(&status);	//父进程在子进程结束后,从wait()调用处开始继续
  printf("child complete,PID: %d\n",a);
}
return 0;

}

终止

  • 主动终止:exit()
  • 被动终止:父进程终止子进程
    • 子进程使用了超过它所分配的资源
    • 分配给子进程的任务,不再需要
    • 父进程退出,而且操作系统不允许无父进程的子进程继续执行(级联终止)

当进程终止时,系统会释放其资源,不过,它位于进程表中的条目还是存在的,直到其父进程调用了wait()

当进程终止,但是父进程未调用wait(),则称其为僵尸进程(一般是个暂时状态)

但是如果父进程没有调用wait()就终止了,这种子进程就成为孤儿进程——Linux解决方法为将init进程作为孤儿进程的父进程,并定期调用wait(),从而释放孤儿进程标识符和进程表条目。

进程通信

进程协作有利于:信息共享、计算加速(多核处理,多个子任务并行执行加速主任务)、模块化、方便(单用户同时执行多个任务)

进程间通信(IPC)机制实现进程协作,有两种方式:

  • 共享内存

    只需要建立共享区时进行内核调用,但是容易产生冲突

    (在多核系统种会有高速缓存一致性问题,效率较低)

  • 消息传递

    无需避免冲突(交换少量数据很有用)但需要多次的系统调用

共享内存系统

POSIX(可移植系统接口,UNIX上系统API总称)具有多种IPC机制,包括共享内存和消息传递

其共享内存方法如下:

shm_open() 创建共享内存对象

ftruncate() 配置对象的大小(以字节为单位)

mmap() 创建内存映射文件,以便包含共享内存对象,返回指向内存映射文件的指针

//server.c
//创建共享内存并写入
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const int size = 4096;
    const char *name ="OS";
    const char *message0 = "hello";
    const char *message1 = "world";

    int shm_fd;
    void *ptr;
    // name:共享内存对象名
    // O_CREAT|O_RDWR:创建共享内存|读写方式打开
    // 0666:目录权限
    shm_fd = shm_open(name,O_CREAT|O_RDWR,0666);
    //共享内存大小设置位size字节
    ftruncate(shm_fd,size);
    /*
    void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
    start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
	length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
	prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
	PROT_EXEC //页内容可以被执行
	PROT_READ //页内容可以被读取
	PROT_WRITE //页可以被写入
	PROT_NONE //页不可访问
	flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
	MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,				   文件实际上不会被更新。
	fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明是匿名映射。
	offset:被映射对象内容的起点。
	*/
    ptr = mmap(0,size,PROT_WRITE,MAP_SHARED,shm_fd,0);
    sprintf(ptr,"%s",message0);
    ptr += strlen(message1);	//调整指针位置继续写入内存
    sprintf(ptr,"%s",message1);

    return 0;
}
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const int size = 4096;
    const char *name ="OS";
    int shm_fd;
    void *ptr;
	//打开方式为只读
    shm_fd = shm_open(name,O_RDONLY,0666);

    ptr = mmap(0,size,PROT_READ,MAP_SHARED,shm_fd,0);

    printf("%s",(char *)ptr);
	//移除共享内存对象
    shm_unlink(name);

    return 0;
}

消息传递系统

  • 直接通信

    • 对称性寻址

      send(P,message):向进程P发送message

      receive(Q,message):从进程Q接受message

    • 非对称性寻址

      send(P,message):向进程P发送message

      receive(id,message):从任何进程接受message,id时通信进程名

  • 间接通信

    两个进程共享一个邮箱

    send(A,message):向邮箱A发送消息

    receive(A,message):从邮箱A接受消息

    其中,邮箱可以为进程或者操作系统拥有(前者进程终止则邮箱消失)

消息传递方式:

  • 阻塞发送:发送进行阻塞,直到消息被接收进程接收
  • 非阻塞发送:发送进程发送完成后,就恢复操作
  • 阻塞接受:接收进程阻塞,直到有消息可用
  • 非阻塞接受:接收进程收到一个有效消息或者空消息

通信进程交换的消息总是驻留在临时队列中,队列有三种实现方法:

  • 零容量:链路中不能有任何消息处于等待状态(这种情况下,发送者应阻塞,直到接收到消息)
  • 有限容量:队列满时,发送者阻塞
  • 无线容量