进程

进程

进程的概念:执行中的程序。
进程是现代分时系统的工作单元。

系统由一组进程组成:操作系统进程执行系统代码,用户进程执行用户代码。通过 CPU 多路复用,所有的进程可以并发执行。通过进程之间的切换,操作系统能使计算机更为高效。

进程概念

进程

进程是执行中的程序,这样的说法显得有点非正式。进程不只是程序代码,程序代码被称为文本段(或代码段)。进程还包括当前活动,通过程序计数器的值和处理器寄存器的内容来表示。另外,进程还包括进程数据段
在内存中的进程如下:

内存中的进程

进程状态

每个进程都处于下列状态之一:
1.new:进程刚进入内存。
2.running:进程正被执行。
3.wait:进程等待某个时间的发生(如完成 I/O 或收到信号)。
4.ready:进程等待分配 CPU。
5.terminated:进程已完成执行。

进程会在这些状态之间切换,状态切换图如下:

创建(new)
执行(running)
阻塞(wait)
就绪(ready)
退出(terminated)

进程状态切换

进程控制块

每个进程在操作系统内都有对应的进程控制块(process control block,PCB)。它包含许多与一个特定进程相关的信息。

PCB作用
进程状态记录进程状态
进程编号每个进程都有一个特定的编号来标记自己,该编号是唯一的
程序计数器计数器表示进程要执行的下个指令的地址
寄存器根据计算机体系结构的不同,寄存器的数量和类型也不同。包括累加器、索引寄存器、堆栈指针、通用寄存器和其他条件码信息寄存器。与程序寄存器一起,这些状态在出现中断时也需要保存,以便以后能正确地执行
内存界限进程可以访问的内存范围
打开文件列表记录进程打开过的文件

进程调度

多道程序设计的目的是无论何时都有进程在运行,从而使 CPU 利用率达到最大化。分时系统使在进程之间快速切换 CPU 以便用户在程序运行时能与其进行交互。

为了达到上述目的,需要设计进程调度,来决定哪个进程进入到内存,CPU 执行何种进程。

调度队列

进程进入内存时,会被加到作业队列中,该队列包括系统中的所有进程。

驻留在内存中就绪的、等待运行的进程保存在就绪队列中。该队列通常用链表来实现,其头节点指向第一个和最后一个 PCB 块的指针。每个 PCB 包括一个指向就绪队列的下一个 PCB 的指针域。

等待特定 I/O 设备的进程列表称为设备队列。每个设备都有自己的设备队列。

新进程开始处于就绪队列。它在就绪队列中等待直到被选中执行或被派遣。当进程分配到 CPU 并执行时,可能发生下面几种事件的一种:
1.进程发出 I/O 请求,并被放到 I/O 队列中(设备队列)。
2.进程可能创建一个新的子进程,并等待其结束。
3.进程可能会由于中断而强制释放 CPU,并被放回到就绪队列。

对于前两种情况,进程最终从等待状态切换到就绪状态,并放回到就绪队列中。进程继续这一循环直到终止,倒是它将从所有队列中删除,其 PCB 和资源将得以释放。

调度程序

进程在其生命周期中会在各种调度队列之间迁移。为了调度,操作系统必须按照某种方式从这些队列中选择进程。进程选择是由相应的调度程序来执行的。

总共有三种类型的调度程序:
1.长期调度程序。对于批处理程序,被提交但没有进入内存的进程被放到大容量存储设备(通常为磁盘)的缓冲池中,保存在那里以便以后执行。长期调度程序从该池选择进程,并装入到内存以准备执行。
2.短期调度程序。从就绪队列中选择程序,并为之分配 CPU。
3.中期调度程序。中期调度的核心思想是能将进程从内存(或从 CPU 竞争)中移出,从而降低多道程序设计的程度。之后,进程能被重新调入内存,并从中断处继续执行。这种方案称之为交换(swapping)。

长期调度程序和短期调度程序的主要差别是它们执行的频率。短期调度程序必须频繁地为 CPU 选择新进程。进程可能执行数毫秒(ms)就会进行 I/O 请求,短期调度程序通常每100ms至少执行一次。由于每次执行之间的时间较短,短期调度程序必须要快。如果需要10ms(进程调度所花的时间)来确定执行一个运行100ms的进程,那么 10/(100 + 10) 的 CPU 时间会用于(或浪费在)调度工作。

长期调度程序执行得并不频繁,在系统内新进程的创建之间可能有数分钟间隔。长期调度程序控制多道程序设计的程度(内存中的进程数量)。如果多道程序的程度稳定,那么创建进程的平均速度必须等于进程离开系统的平均速度。因此,只有当进程离开系统后,才可能需要长期调度程序。由于每次执行之间时间间隔得较长,长期调度程序能使用更多的时间来选择进程。

绝大多数进程可分为:I/O 为主或 CPU 为主。长期调度程序应该选择一个合理的包含 I/O 为主的和 CPU 为主的组合进程。

上下文切换

中断使 CPU 从当前任务改变为运行内核子程序,这样的操作在通用系统中发生的很频繁。

当发生一个中断时,系统需要保存当前运行进程的上下文,从而能够在之后重新运行。进程的上下文用进程的 PCB 表示。

当发生上下文切换时,内核会将旧进程的状态保存在其 PCB 中,然后装入经调度要执行的并已保存的新进程的上下文。


进程操作

绝大多数系统内进程能并发执行,他们可以动态创建和删除。

进程创建

能通过创建进程系统调用(create-process system call)创建多个新进程。创建进程的进程称为父进程,被创建的进程称为子进程。

每个进程都可以创建新进程,而所有的进程都有一个相同的祖先,这就形成了进程树。

大多数操作系统(包括 UNIX 和 Windows 系列操作系统)根据一个唯一的进程标识符(process identifier,pid)来标志进程,pid 通常是一个整数值。

通常,进程需要一定的资源(如 CPU 时间、内存、文件、I/O 系统)来完成任务。在一个进程创建进程时,子进程可能从操作系统那里直接获得资源,也可能只从其父进程那里获得资源。进程创建时,除了各种物理和逻辑资源之外,还需要初始化数据(输入,在 Windows 上体现在点击可执行文件时,会将文件路径输入到新进程中),这部分由父进程传递给子进程。

当进程创建新进程时,有两种后续可能:
1.父进程和子进程并发执行。
2.父进程等待,直到某个子进程或全部子进程执行完毕。

新进程的地址空间也有两种可能:
1.子进程是父进程的复制品
2.子进程装入另一个新程序

Unix 系列操作系统创建子进程:

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

int main()
{
    int i = 1;
    pid_t pid;
    // vfork和fork有所区别,这里调用vfork更节省资源
    pid = vfork();
    if(pid < 0)
    	exit(1);
    else if(pid == 0)
    {
        printf("i: %d\n", i++);
        printf("Child process: %d\n", getpid());
        execlp("ls", "ls", "-l", NULL);
        exit(1);
    }
    else
    {
        // vfork 必定阻塞父进程,因此不需要wait或waitpid
        // wait(NULL);
        // waitpid(-1, NULL, NULL);
        printf("i: %d\n", i++);
        printf("Parent process: %d\n", getpid());
    }
    exit(0);
}

进程终止

当进程执行完最后的语句并使用系统调用 exit() 请求操作系统删除自身时,进程终止。这时,进程可以返回状态值到父进程(父进程通过调用 wait 或 waitpid 获得)。所有进程资源会被操作系统释放。

如果父进程终止,那么所有子进程会以 init 进程作为父进程。


进程间通信

操作系统内并发执行的进程可以是独立进程或协作进程。如果一个进程不能影响其他进程或被其他进程所影响,那么该进程是独立的;反之该进程是协作的。

协作进程需要一种进程间通信机制(interprocess communication,IPC)来允许进程相互交换数据和信息。

进程间通信有两种基本模式:
1.共享内存
2.消息传递

消息传递比共享内存更易于实现,但共享内存允许以最快的速度进行方便的通信。

共享内存

内核会在物理空间上开辟一个共享内存区域,进程可以指向该共享内存区域,然后读写数据。

/*
共享内存本身不提供同步机制,这意味着它不能保证当某个进程在往共享内存区域写东西的时候,其他进程不会读这个共享内存区域。
需要自己用信号量做好同步
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>

int main()
{
    int segment_id;
    char* buf;
    int buflen = 1024;

    segment_id = shmget(IPC_PRIVATE, buflen, 0);
    if(segment_id < 0)
        exit(1);

    buf = (char*)shmat(segment_id, NULL, 0);

    pid_t pid = fork();
    if(pid < 0)
        exit(1);
    else if(pid == 0)
    {
        snprintf(buf, buflen, "Hello World!");
        
        shmdt(buf);
        if(shmctl(segment_id, IPC_RMID, NULL) < 0)
            exit(1);
            
        exit(0);        
    }
    else
    {
        wait(NULL);

        printf("%s\n", buf);
        
        shmdt(buf);
        if(shmctl(segment_id, IPC_RMID, NULL) < 0)
            exit(1);
        
        exit(0);
    }
}

消息传递

消息传递大致有以下5种方式:
1.半双工 Unix 管道
2.FIFOs(有名管道)
3.消息队列
4.信号量
5.网络socket

半双工 Unix 管道

该管道又称为无名管道,主要用于有血缘关系进程之间的通信。
主要函数为:

int pipe(int pipefd[2]); // 创建管道
pipefd[0] 为读端
pipefd[1] 为写端

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

int main()
{
    int pipefd[2];
    if(pipe(pipefd) < 0)
        exit(1);

    pid_t pid = fork();
    if(pid < 0)
        exit(1);
    else if(pid == 0)
    {
        if(close(pipefd[0]) < 0)
            exit(1);

        char msg[13] = "Hello World!";
        char* ptr = msg;
        int msglen = strlen(msg);
        int n;
        while(msglen)
        {
            if((n = write(pipefd[1], ptr, msglen)) < 0)
                exit(1);
            else
            {
                ptr += n;
                msglen -= n;
            }
        }

        if(close(pipefd[1]) < 0)
            exit(1);
        exit(0);
    }
    else
    {
        wait(NULL);

        if(close(pipefd[1]) < 0)
            exit(1);

        char buf[1024];
        memset(buf, '\0', 1024);
        char* ptr = buf;
        int n;
        int buflen = 1024;
        while(1)
        {
            if((n = read(pipefd[0], ptr, buflen)) == 0)
                break;
            else if(n < 0)
                exit(1);
            else
            {
                ptr += n;
                buflen -= n;
            }
        }

        printf("%s\n", buf);
        if(close(pipefd[0]) < 0)
            exit(1);
        
        exit(0);
    }
}
FIFOs(有名管道)

有名管道是一个设备,以 FIFO 的文件形式存储于文件系统中。因此,即使进程之间没有血缘关系,也可以通过有名管道通信。
主要函数为:

int mkfifo(const char* pathname, mode_t mode);
int mknod(const char* pathname, mode_t mode, dev_t dev);

创建完之后,对于有名管道文件的读写操作和对普通文件的读写操作相似,这里不测试代码了。

与一般文件稍微有些不同的是,调用 open 打开有名管道文件会有阻塞的情况。只有当读端和写端都被打开时,才不阻塞。

消息队列

消息队列本质是由内核创建的用于存放消息的链表。进程通过共享操作同一个消息队列,实现进程间通信。每一个消息队列都有唯一的标识符

每个消息由两部分组成:
1.消息编号,用于识别消息。
2.消息正文,存储信息内容

创建(获得)消息队列

通过函数 msgget 实现。
函数 msgget 如下:

int msgget(key_t key, int msgflg);
// key, 用于生成消息队列的标识符
// msgflg,指定创建时的原始权限,比如0666。除了原始权限,还需要指定 IPC_CREAT选项。msg_id = msgget(key, 0666 | IPC_CREAT);
//	成功返回消息队列标识符,失败返回负数
发送消息

进程先封装一个消息包,消息包的结构如下:

struct msgbuf
{
	long mtype;	// 消息编号,> 0
	char mtext[msgsz];	// 消息正文
};

然后再调用 msgsnd 发送消息到消息队列上。
msgsnd 函数如下:

int msgsnd(int msg_id, const void* msgp, size_t msgsize, int msgflg);
// msg_id,消息队列标识符
// msgp,消息包
// msgsize,消息正文的大小
// msgflg, 0的话为阻塞;IPC_NOWAIT的话为非阻塞
// 成功返回0,失败返回负数
接受消息

从消息队列上接受消息,通过函数 msgrcv 实现。
函数 msgrcv 如下:

ssize_t msgrcv(int msg_id, void* msgp, size_t msgsize, long msgtyp, int msgflg);
// msg_id,消息队列的标识符
// msgp,用来存放消息包的地址
// msgsize,消息正文的大小
// msgtyp,需要接受的消息的消息编号
// msgflg, 为0的话阻塞;IPC_NOWAIT的话非阻塞
// 成功返回消息正文的字节数,失败返回负数
释放消息队列

通过函数 msgctl 实现。
函数 msgctl 如下:

int msgctl(int msg_id, int cmd, struct msqgid_ds* buf);
// msg_id,消息队列的标识符
// cmd,控制选项
// buf,存放属性信息
// 成功返回0,失败返回负数
代码例子
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <string.h>

#define MSGFILE "./msgfile"

int main()
{
    key_t key;
    key = ftok(MSGFILE, 'a');
    int fd;

    if((fd = msgget(key, 0666 | IPC_CREAT)) < 0)
        exit(1);

    struct msgbuf buf;
    memset(&buf, 0, sizeof(buf));
    buf.mtype = 1;
    strncpy(buf.mtext, "Hello World!", 12);

    if(msgsnd(fd, &buf, 1024, 0) < 0)
        exit(1);

    ssize_t msgsize;
    struct msgbuf rbuf;
    memset(&rbuf, 0, sizeof(rbuf));

    if((msgsize = msgrcv(fd, &rbuf, 1024, 1, 0)) < 0)
        exit(1);

    printf("%s\n", rbuf.mtext);

    if(msgctl(fd, IPC_RMID, NULL) < 0)
        exit(1);

    exit(0);
}
信号量

信号量本质上是一个计数器,用于多进程对共享数据对象的读取。与管道和消息队列不同,信号量不以传送数据为主要目的,而是用来保护共享资源(包括硬件资源)。

工作原理

有两个操作 P(sv) 和 V(sv),它们的作用如下:

  1. P(sv),如果 sv 的值大于零,就减一;如果 sv 的值为零,就挂起进程。
  2. V(sv),如果有其他继承因等待 sv 而挂起,就让它恢复执行;如果没有,就给 sv 加一。

PV操作都为原子操作。

二元信号量

二元信号量(Binary Semaphore),也称为互斥锁。它只有两个计数,1 和 0,分别代表非占用和占用。

创建信号量

函数 semget 用来创建信号量,如下:

int semget(key_t key, int nsems, int semflg);
// key, 用来生成信号量集标识符(同消息队列的key值一样,通常由 ftok 函数生成)
// nsems,指定信号量集中需要的信号量数目,它的值几乎总是1
// semflag,同消息队列一样,一般是 IPC_CREAT 与文件权限做或操作
删除信号量

函数 semctl 用来删除信号量,如下:

int semctl(int sem_id, int semnum, int cmd, ...);
// sem_id,信号量集标识符
// semnum,信号量在信号集里的编号
// cmd,控制位,同消息队列一样,IPC_RMID用来删除信号量
信号量操作

函数 semop 用来操作信号量,如下:

int semop(int sem_id, struct sembuf* sops, size_t nops);
// sem_id, 信号量集标识符
// nops: 操作信号量的个数,也就是结构数组sops里结构的数量
// sembuf 结构如下
struct sembuf
{
	short sem_num;	// 信号量在信号集里的编号
	short sem_op;	// 信号量的操作,-1为P,+1为V(也可以为其他值,表示对信号量的改变量)
	short sem_flg;	// 控制位,0阻塞;IPC_NOWAIT不阻塞;SEM_UNDO,程序结束时,保证信号值会被重设为semop调用前的值,避免程序异常结束对信号量的影响。
};
代码例子
/*
删除信号量会导致另一进程对信号量的操作出现错误,
不过本代码是为了直观表现对于二元信号量,PV操作都是成对出现的。
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <string.h>
#include <signal.h>

#define SEMFILE "./semfile"

typedef void (* Sigfunc) (int);

void signal_handler(int signo)
{
    printf("signal_handler\n");
    wait(NULL);
}

int main()
{
    Sigfunc handler = signal(SIGCHLD, signal_handler);

    key_t key = ftok(SEMFILE, 'a');
    int fd;
    if((fd = semget(key, 1, IPC_CREAT | 0666)) < 0)
        exit(1);

    struct sembuf buf;
    memset(&buf, 0, sizeof(buf));
    buf.sem_num = 0;
    buf.sem_op = 1;
    buf.sem_flg = SEM_UNDO;
    if(semop(fd, &buf, 1) < 0)
        exit(1);

    pid_t pid = fork();
    if(pid < 0)
        exit(1);
    else if(pid == 0)
    {  
        buf.sem_op = -1;
        if(semop(fd, &buf, 1) < 0)
            exit(1);
        printf("Child process P\n");
        buf.sem_op = 1;
        if(semop(fd, &buf, 1) < 0)
            exit(1);
        printf("Child process V\n");

        sleep(10);
        if(semctl(fd, 0, IPC_RMID) < 0)
            exit(1);

        printf("Child process ctl\n");
        exit(0);
    }
    else
    {
        buf.sem_op = -1;
        if(semop(fd, &buf, 1) < 0)
            exit(1);
        printf("Parent process P\n");
        buf.sem_op = 1;
        if(semop(fd, &buf, 1) < 0)
            exit(1);
        printf("Parent process V\n");

        sleep(10);
        if(semctl(fd, 0, IPC_RMID) < 0)
            exit(1);

        printf("Parent process ctl\n");
        exit(0);
    }
}
网络socket

网络socket 应该是最常见的通信方式了(?可能对我来说是的)。除了在本地进程间通信,也可以在端与端的进程间通信。这边就不展开讲了,可以看链接: Tcp Daytime获取客户端 简单了解一下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值