操作系统 - 进程与线程
标签(空格分隔): 操作系统
[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)
- 读写锁
- 条件变量