操作系统

进程与线程的区别

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  2. 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  3. 进程是CPU资源分配的最小单位,线程是CPU调度的最小单位;
  4. 系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、IO设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销
  5. 线程间比进程间通信更容易。由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预。
  6. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂
  7. 进程间不会相互影响 ,而一个线程挂掉将导致整个进程挂掉。

进程间通信的方式

进程间通信主要包括管道系统IPC(包括消息队列、信号量、共享内存、信号等)、以及套接字socket

1.管道:

管道主要包括无名管道命名管道。管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

1.1 普通管道PIPE(无名管道):

1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,fd[0] 和fd[1],0是读端,1是写端。

2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

Linux下实现代码

int main()
{
    char buf[1024]="change world!\n";
    int fds[2];
    if(pipe(fds) == -1)
        perror("pipe"),exit(1);
    pid_t pid = fork(); //创建子进程
    if(pid == 0)//如果是父进程
    {
        close(fds[0]); //关闭管道读描述符
        if(write(fds[1],buf,1024)==-1) //写进管道
            perror("write"),exit(1);

        close(fds[1]); 
        exit(1);
    }
    else
    {
        memset(buf,0x00,1024);
        close(fds[1]); //关闭管道写描述符
        if(read(fds[0],buf,1024)==-1) //从管道读内容
            perror("read"),exit(1);
        if(write(1,buf,1024)==-1)
            perror("write"),exit(1);
        close(fds[0]);
        exit(1);
    }
    return 0;
}

1.2 命名管道FIFO:

1)FIFO可以在无关的进程之间交换数据

2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

Linux下实现代码

在Linux系统中,使用下边命令创建命名管道

mkfifo filename
[centos@localhost fifo]$ mkfifo myfifo
[centos@localhost fifo]$ ls
myfifo
[centos@localhost fifo]$ file myfifo
myfifo: fifo (named pipe)

也可以在程序内部创建

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

int main()
{
    mkfifo("my.p", 0644);
    return 0;
}

从命名管道中读入读出

#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

//读入示例
int main()
{
    mkfifo("my.p",0664);//八进制0664 = 110 110 100
    int outfd = open("my.p",O_WRONLY);
    if(outfd == -1)
        perror("open my.txt"),exit(1);
    char buf[1024]={};
    int n = 0;
    while(fgets(buf,1024,stdin))
    {   
        write(outfd,buf,1024);
        memset(buf,0x00,1024);
    }  
    close(outfd);
}

//读出示例
int main()
{
    int infd = open("my.p",O_RDONLY);
    if(infd==-1)
        perror("open my.p"),exit(1);
    char buf[1024]={};
    int n = 0;
    while((n = read(infd,buf,1024))>0)
    {
        write(1,buf,n);
        memset(buf,0x00,1024);
    }
    close(infd);                                                                                                         
    unlink("my.p"); //删除管道
    return 0;
}

总结:

匿名管道由pipe函数创建并打开。命名管道由mkfifo函数创建,打开⽤用open。FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的⽅方式不同,一但这些工作完成之后,它们具有相同的语义。

参考资料.Blog龙跃十二.利用管道实现进程间通信

2.系统IPC(Inter-Process Communication)

2.1 消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;

特点:

1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取

Linux下消息队列实现主要通过四个函数,msgget(),msgsnd(),msgrcv(),msgctl()。

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

//调用成功返回一个key
key_t ftok(const char *pathname, int proj_id);

//创建和获取消息队列
int msgget(key_t key, int msgflg);
//key : 消息队列关联的键,即id, 使用ftok()产生,不同进程通过相同key可以得到相同消息队列
//msgflg :消息队列的建立标志和存取权限
//IPC_CREAT 单独使用是如果没有就创建 
//IPC_EXCL + IPC_CREAT 是已经存在则执行失败
//返回创建的消息队列的标识符msqid,msqid是IPC对象内部名,失败返回-1
//使用的例子
int open_queue(int keyval)
{
    int qid;
    qid = msgget(keyval, IPC_CREAT | 0666);
    if (qid == -1)
    {
        perror("Failed in calling msgget");
        return -1;
    }
    return qid;
}

//发送一条消息到指定的消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflag);
//msqid为消息队列标识符
//msgp指向准备发送消息的指针
//msgsz上述指针指向消息的长度
//msgflag默认为0(IPC_NOWAIT),表示阻塞,1表示非阻塞。阻塞表示如果队列满了就一直等到有地方,如果非阻塞满了,就直接返回-1。
//消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型(协议必须的部分)
//实际例子
struct msgbuf
{
    long mtype;
    char mtext[1024];
}

int sendMsgQueue(int msg_id, int who, char* msg)
{
    struct msgbuf buf;
    buf.mtype = who;
    strcpy(buf.mtext, msg);//前目的,后源

    if (msgsnd(msg_id, (void*) &buf, sizeof(buf.mtext), 0) < 0)
    {
        perror("Fail to send message to MsgQueue!");
        return -1;
    }
    return 0;
}

//从指定消息队列接收一条消息
int msgrcv(int msqid, const void* msgp, size_t msgsz, long msgtype, int msgflg);
//参数和msgsnd相同

//例子
int recvMsgQueue(int msg_id, int recvType, char out[])
{
    struct msgbuf buf;
    int size = sizeof(buf.mtext);
    if (msgrcv(msg_id, (void*)&buf, size, recvType, 0) < 0)
    {
        perror("Fail to receive message from MsgQueue!");
        return -1;
    }

    strncpy(out, buf.mtext, size);//将特定大小的字符串复制
    out[size] = 0;//字符串设置为'\0'
    return 0;
}

//消息控制
int msgctl(int msqid, int cmd, struct msgid_ds *buf);
//msqid是由msgget返回的消息队列id
//cmd有三种值 
//IPC_RMID 删除消息队列
//PIC_STAT 把msgid_ds结构中的数据设置为消息队列的当前关联值
//IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msgid_ds数据结构中给出的值

int destoryMsgQueue(int msg_id)
{
    if (msgctl(msg_id, IPC_RMID, NULL) < 0)
    {
        perror("Remove MsgQueue Fail!");
        return -1;
    }
    return 0;
}
#ipcs 显示IPC资源 -q是消息队列, -m是共享内存
#ipcrm 手动删除IPC资源
[root@localhost code]# ipcs -q
------ Message Queues --------                                                 key        msqid      owner      perms      used-bytes   messages
0x66020001 524288     root       666        0            0

[root@localhost code]# ipcrm -q 524288
[root@localhost code]# ipcs -q
------ Message Queues --------                                                 key        msqid      owner      perms      used-bytes   messages

[root@localhost code]#

CSDN. Linux进程间通信之消息队列

CSDN. Linux进程通信之消息队列-项目实践

2.2 共享内存(Shared Memory)

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

特点:

1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取。

2)因为多个进程可以同时操作,所以需要进行同步。

3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

Linux下共享内存机制实现代码,因为是IPC所以很多差不多:

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

//建立(获得)一块共享存储区,返回共享存储区的描述符shmid,新建立后初始化为0
//key还是ftok函数生成的
int shmget(key_t key, size_t size, int shmflg);
//这里的key和size与消息队列相同
//shmflg有 IPC_CREAT IPC_EXCL用法同消息队列
//SHM_HUGETLB 使用“huge pages”来分配共享区段
//SHM_NORESERVE 不要为共享区段保留交换空间

//对共享存储区shmid执行操作cmd,对其状态进行修改和控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//cmd有五种,除了消息队列的三种,还多了两种
//IPC_STAT IPC_SET IPC_RMID 删除的时候会等待最后一个使用该存储区的进程终止,但是标识符会立刻删除,无法再使用shmat与该段相连
//SHM_LOCK SHM_UNLOCK 锁住和解锁共享存储区,需要superuser权限

//获得了shmid后,系统需要使用shmat将该共享存储区附接到用户给定的某个进程的虚拟地址
void *shmat(int shmid, const void *addr, int flag);
//addr指定共享内存出现在进程地址的什么位置,指定为NULL可以让内核自己决定
//flag是对于数据的操作,SHM_RDONLY(010000)是只读,其他为读写方式
//成功返回指向共享存储段的指针,错误返回-1(0xffffffff)

//当进程不再需要一个共享存储段时,可以使用shmdt
int shmdt(void * addr);
//addr是调用shmat的返回值
//成功返回0,错误返回-1

//实际例子
struct Info{
    char name[100];
    int age;
};

int main()
{
    key_t key = ftok("/tmp", 66);
    int shmid = shmget(key, sizeof(Info), IPC_CREAT|0666);
    Info *p;
    p = (Info *)shmat(shmid, NULL, 0);
    char name[100] = "ScarofSky";
    int age = 25;
    strcpy(p -> name, name);
    p -> age = age;
    if (shmdt(p) == -1)
    {
        perror("Dettach Fail!");
        return -1;
    }

    return 0;
}

总结:相当于对于共享内存的读写,转换为当前进程中虚拟地址(指针)的读写。

CSDN.Linux下共享内存编程

2.3 信号量semaphore

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据

特点:

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作

  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数

  4. 支持信号量组。

PV操作的具体意义:

  • P(s):将信号量value值减1,若结果小于0,则执行P操作的进程被阻塞,若结果大于等于0,则执行P操作的进程将继续执行。
  • V(s):将信号量的值加1,若结果不大于0,则执行V操作的进程从信号量s有关的list所知队列中释放一个进程,使其转化为就绪态,自己则继续执行,若结果大于0,则执行V操作的进程继续执行。

PV操作先对value值加减,再判断和0的关系,之后再决定阻塞和唤醒,是上述逻辑。信号量在Linux中有两种规范,一个是System V标准,另一个是Posix标准。如下的具体代码只涉及Posix标准,System V只列出。

//System V 标准API

int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sops, size_t nsops);
int semctl(int sem_id, int sem_num, int cmd);

//Posix 标准API
#include "semaphore.h"

//初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem为指向信号量结构的一个指针
//pshared != 0在进程间共享,pshared == 0 只在当前进程的所有线程共享
//value是初始值

//等同于V操作
int sem_post(sem_t *sem);
//调用后value ++ , 线程调度策略决定唤醒哪个阻塞的线程

//等同于P操作
int sem_wait(sem_t *sem);
//如果信号量值大于0, 则值-1, 如果值小于等于0,则阻塞当前进程
//if (sem > 0) sem -- ;
//else if (sem <= 0) 
//{
//    sem -- ;
//    sleep();
//}

CSDN. Linux信号量操作

2.4 信号signal

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

Blog.sky

3.套接字SOCKET:

socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。

CSDN.Linux网络编程 套接字编程

线程间通信的方式

临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;

互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问

信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。

事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

死锁的必要条件和解决方法

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:

  1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
  2. 占有和等待:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源。
  3. 非抢占:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。
  4. 循环等待:进程发生死锁后,必然存在一个进程-资源之间的环形链。

死锁的处理方法是死锁预防死锁避免死锁检测死锁恢复

  • 死锁预防的方法:
  1. 资源一次性分配,从而剥夺占有并等待条件。当一个进程申请一个资源时,它不能占有其他资源。缺点是资源利用率低和可能发生饥饿,当资源一次性分配之后,可能有些最后才用到;如果进程需要多个常用资源,可能会发生饥饿导致一直分配不到资源,一直等待。
  2. 抢占式协议:如果一个进程占有资源并申请另一个不能立即分配的资源,那么现在已经分配的资源允许其他进程抢占。换句话说,这些资源被隐式释放了。如果有新的进程请求资源,那么系统先考虑自由资源,再考虑等待中进程的资源,如果都不够,那么该进程也进入等待。
  3. 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件。
  • 死锁避免的方法:获得以后进程如何申请资源的附加信息,由操作系统来综合考虑如何分配资源,使用已有的死锁避免算法。
  1. 安全状态
  2. 资源分配图算法(每种资源都有单个实例)
  3. 银行家算法(每种资源有多个实例)
  • 死锁检测和恢复的方法:系统提供检测算法 + 恢复算法

缺页置换算法

当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:

  1. 先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后顺序排列。
  2. 最近最少使用(LRU)算法:置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。被使用的页就会被刷新,放到最近的位置,长时间未被使用的就会慢慢排到最后。

并发和并行

  • 并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核CPU上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。
  • 并行(parallelism):指严格物理意义上的同时运行,比如多核CPU,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

大端序和小端序

大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。

//判断系统是大端序还是小端序
int fun1()
{
    union  test
    {
        int i;
        char c;
    };

    test t;
    t.i = 1;
    return (t.c == 1);
}

比如0x12345678如果是小端序,存储地址从低到高应该是78 56 34 12,如果是大端序,从低到高地址存储应该是12 34 56 78。

大端序:

低地址 ——————————> 高地址
0x12 | 0x34 | 0x56 | 0x78

小端序:

低地址 ——————————> 高地址
0x78 | 0x56 | 0x34 | 0x12

内核态和用户态

内存管理

CPP到EXE过程

预处理

  1. 删除注释
  2. 宏替换,#define删除并且替换所有的宏定义位置的值
  3. 处理所有条件预编译指令,#if #ifdef #endif #elif #else,处理#include,将包含的文件直接复制到当前位置。保留#pragma的编译指令,编译器会使用
  4. 添加行号和文件名表示,以便于调试时能显示编译错误或警告产生。

编译过程

  1. 词法分析
  2. 语法分析
  3. 语义分析与中间代码生成
  4. 优化(对于中间代码)
  5. 目标代码生成

目标代码的形式可以是绝对指令代码可重定位的指令代码汇编指令代码

如目标代码是绝对指令代码,则这种目标代码可立即执行。

如果目标代码是汇编指令代码,则需汇编器汇编之后才行运行。

现在多数实用编译程序所产生的目标代码都是一种可重定位的指令代码。这种目标代码在运行前必须借助于一个连接装配程序(链接器)把各个目标模块(包括系统提供的库函数)连接在一起,确定程序变量(或常数)在主存中的位置,装入内存中指定的起始地址,使之成为一个可以运行的绝对指令代码程序

Blog.CSDN 编译过程的五个阶段

汇编

将汇编语言翻译成二进制代码,构建可重定位的二进制文件(.o/.obj文件)。

链接

链接主要是将多个文件编译出来的.o文件合成最终的可执行程序,这样可以将程序按照模块开发,降低开发的难度。这样会带来问题是不同文件会互相使用其他文件中定义的全局变量和函数,编译时只能解决当前文件中地址,但是真实运行时的具体地址,要靠链接器来决定。

可重定位目标文件中包含代码和数据,即.text段.data段.bss段。同时也包括符号表来提供链接时需要的信息。

  1. 合并段。
  2. 调整段地址偏移量和段长度。
  3. 合并所有的符号:进行符号解析。
  4. 空间与地址的分配。
  5. 符号重定位。

可执行文件加载到内存之后的分布:

Blog.snsart 文件的链接过程


庄敬日强,功不唐捐。