操作系统 - 进程与线程
标签(空格分隔): 操作系统
[TOC]
进程与线程之间的联系与区别
进程是系统资源分配的基本单位例如内存资源,两个进程之间内存是隔离的,而线程是 CPU 调度的基本单位;
进程体现的是系统的并发能力,线程体现系统的并行能力;
线程是进程的一部分,一个进程中可以包含多个进程;
进程和线程的区别 一些回答也可以简要的说明。
Linus 在邮件中的讨论说明,这个回答中提到的 执行环境
(context of execute, COE)可以了解。
进程控制
文章中函数参数的具体信息可以从《Unix 环境高级编程》中得知,在此不详述。 《Unix 环境高级编程》好难看,好难看……
进程标识符
每一个进程都有一个非负整性表示唯一的进程 ID, 因为进程 ID 标识符总是唯一,常将其用来作其他标识符的一部分以保证其唯一性 虽然是唯一的,但是进程 ID 可以重用,当一个进程终止后,其进程 ID 就可以再次使用 《Unix 环境高级编程》
fork函数
一个现有进程可以调用 fork()
创建新进程;
fork
函数原型如下:
#include<unistd.h>
pid_t fork(void);
fork
一次调用有两个返回值,其中父进程的返回值是子进程的 pid
, 其返回值大于 0 ,因为没有函数可以提供子进程的进程 ID ,这样设计可以提供父进程子进程的 pid
。
子进程和父进程继续执行 fork 调用之后的指令,子进程是父进程的副本。注意子进程所拥有的副本,父、子进程并不共享这些存储空间部分。父子进程共享正文段
一个fork的面试题 涉及 fork 的特性。
wait 和 waitpid 函数
一个已经终止的进程但是其父进程未获取子进程的终止状态,释放进程占用资源的进程称为僵死进程;当一个子进程在父进程之前终止,父进程调用 wait 或 waitpid 函数获取子进程的状态。 函数原型如下:
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t wait(pid_t pid, int * statloc, int options);
区别如下:
- 子进程未终止,调用
wait
使调用者阻塞,waitpid
有一选项可以不阻塞 waitpid
有若干选项可以控制它所等待的进程 参数信息可以查看《Unix 环境高级编程》
进程间通信 (IPC)
匿名管道(pipe)
pipe 管道存在以下的缺陷:
- 数据流单向流动
- 只能用于具有公共祖先的进程
函数原型如下:
#include<unistd.h>
int pipe(int filedes[2])
其中 filedes[0]
是读端口, filedes[1]
是写端口。
Demo:
int main()
{
int fd[2];
pid_t pid;
if(pipe(fd) < 0)
{
fprintf(stderr, "pipe error");
exit(EXIT_FAILURE);
}
if( (pid= fork()) < 0)
{
fprintf(stderr, "fork error");
exit(EXIT_FAILURE);
}
else if(pid > 0)
{// parent
wait(NULL);//子进程未终止,则阻塞
printf("Parent process\n");
char buf[BUFSIZ];
int cnt;
cnt = read(fd[0], buf, BUFSIZ);
buf[cnt] = '\0';
printf(" read msg <%s> from pipe\n", buf);
close(fd[0]);
exit(EXIT_SUCCESS);
}
else
{//child
printf("Child process\n");
write(fd[1], "Hello world", 11);
close(fd[1]);
exit(EXIT_SUCCESS);
}
return 0;
}
命名管道(FIFO)
FIFO 也被称为命名管道,弥补了匿名管道只能在具有公共祖先进程中使用的缺陷。FIFO是一种类似文件结构,所以可以用 read/write
函数读写。
创建 FIFO 管道
#include<sys/stat.h>
mkfifo(const char *file_path, int modes)
Demo
#define FILE_PATH "./fifo"
int main()
{
pid_t pid;
//创建 FIFO 管道,进程只需要知道文件路径,就可以通信
mkfifo(FILE_PATH, 0777);
if((pid = fork()) < 0)
{
fprintf(stderr, "fork error");
exit(EXIT_FAILURE);
}
else if( pid > 0)
{//parent
wait(NULL);
printf("Parent process\n");
// 类文件结构,可以用有关文件函数操作 FIFO
int fd = open(FILE_PATH, O_RDONLY);
char buf[BUFSIZ];
read(fd, buf, BUFSIZ);
printf("read msg <%s> from fifo\n", buf);
close(fd);
exit(EXIT_SUCCESS);
}
else{//child
printf("Child process\n");
int fd = open(FILE_PATH, O_WRONLY);
write(fd, "Hello world", 11);
close(fd);
exit(EXIT_SUCCESS);
}
return 0;
}
当打开一个 FIFO 管道时,可以设置非阻塞 O_NONBLOCK 模式, 这个标志位,open的时候可以立即返回,一般不设置。 一个FIFO,可能有多个写程序;一般应该做好写进程间的同步,以及写操作的原子化,要么全写,要么不写。
消息队列
与消息队列有关函数
#include<sys/msg.h>
int msgget(key_t key, int flag);
int msgsnd(int msqid, const void *prt, size_t nbytes, int flag);
int msgrcv(int msqid, void *prt, size_t nbytes, long type, int flag);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//消息结构
struct mymesg{
long type; //消息类型
char text[BUFSIZ]; //消息内容
};
Demo
int main()
{
pid_t pid;
int qid;
key_t key;
//创建消息队列
qid = msgget(key, IPC_CREAT|0777);
if( (pid=fork()) < 0)
{
fprintf(stderr, "fork error\n");
exit(EXIT_FAILURE);
}
else if (pid > 0)
{//parent
wait(NULL);
struct mymsg msg_r;
// 接收第一个类型为 10 的消息,并设置为非阻塞,立即返回
int len = msgrcv(qid, &msg_r, BUFSIZ, 10, IPC_NOWAIT);
msg_r.text[len] = '\0';
printf("recieve type %ld, msg <%s> from msg queue\n", msg_r.type, msg_r.text);
// 删除消息队列
msgctl(qid, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}
else
{//child
struct mymsg msg_w;
msg_w.type = 10;
strcpy(msg_w.text, "Hello world");
msgsnd(qid, &msg_w, strlen(msg_w.text), IPC_NOWAIT);
exit(EXIT_SUCCESS);
}
return 0;
}
消息队列并不是粗暴的先进先出的获取消息,可以根据消息的类型信息获取消息,由此我们可以制定消息的优先级别。同时不同的进程可以共享这个消息队列,共享的方式是传递队列的ID, 或者是创建消息队列的 key。
信号量
信号量其实是操作系统中的同步原语,控制进程之间的同步关系, 是 PV 操作的实际实现。同时 与信号量有关的函数:
#include<sys/sem.h>
int semget(key_t key, int nsems, int flag);
int semop(int semid, struct sembuf sems[], size_t nops);
int semctl(int semid, int semums, int cmd);
struct sembuf{
unsigned short sem_num;
short sem_op;
short sem_flg;
};
系统中信号量的定义是一个信号量集合,如 semget
创建指定 nsems
数量的信号量,所以当操作信号量时需要指定 sembuf.sem_num
编号。 sembuf.sem_op
值体现信号量操作。
Demo
int main()
{
pid_t pid;
key_t key;
int semid = semget(key, 1, IPC_CREAT|0777);
if((pid = fork()) < 0)
{
fprintf(stderr, "fork error\n");
exit(EXIT_FAILURE);
}
else if (pid >0)
{//parent
int i = 3;
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = 1;
sb.sem_flg = 0;
semop(semid, &sb, 1);
while( i> 0)
{
sb.sem_op = -1;
semop(semid, &sb, 1);
printf("Parent process\n");
i -= 1;
sb.sem_op = 1;
semop(semid, &sb, 1);
}
semctl(semid, 0, IPC_RMID);
exit(EXIT_SUCCESS);
}
else
{
int i = 2;
struct sembuf sb;
while( i > 0)
{
sb.sem_num = 0;
sb.sem_op = -1;
sb.sem_flg = 0;
semop(semid, &sb, 1);
printf("Child process\n");
i -= 1;
sb.sem_op = 1;
semop(semid, &sb, 1);
}
exit(EXIT_SUCCESS);
}
}
共享内存
共享内存允许多个进程可以使用共同的一块内存区域,因为不需要在服务端客户端之间复制,所以共享内存是最快的 IPC 之一。就如 FIFO 一样,我们需要利用信号量或者其他进程间同步的手段,控制进程的读写操作。
共享内存相关的函数如下:
#include<sys/shm.h>
int shmget(key_t key, size_t size, int flag);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
void *shmat(int shmid, const void * addr, int flag);
Demo
int main()
{
pid_t pid;
key_t key;
int shmid = shmget(key, BUFSIZ,IPC_CREAT | 0666);
if( (pid = fork()) < 0)
{
fprintf(stderr, "fork error");
exit(EXIT_FAILURE);
}
else if (pid > 0)
{//parent
wait(NULL);
char *addr;
addr = shmat(shmid,0, 0);
printf(" msg <%s>\n", addr);
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}
else
{//child
char* addr;
addr = shmat(shmid, 0, 0);
strcpy(addr,"Hello world");
exit(EXIT_SUCCESS);
}
}
以上几种进程间通讯方式(除管道外),都需要 key_t key
作为键,这个值将会被内核修改,使得 IPC 对象与键对应,进程间利用这个键可以访问到 IPC 对象, Demo 程序示例是父子进程之间共享 ID 的方法获取对象的, ***get
函数是可以创建一个对象,也可以打开一个已有对象。***ctl
函数都是根据命令做相应操作的函数。
线程同步与互斥
由于线程是 CPU 的调度单位, 进程才是资源的分配单位,所以同一个进程之间的线程资源是共享的;线程则主要了解数据的线程安全性,线程之间的同步以及锁的概念。 线程之间同步方法有以下几种:
- 互斥量(mutex)
- 读写锁
- 条件变量