Linux操作系统(五):进程管理组件

在这里插入图片描述

1.进程和线程

进程(Process):由代码编译而来的二进制可执行文件,CPU会将其装载到内存中然后执行程序中的每一条指令,整个进行中的程序即进程。

单核处理器的进程并发和多核处理器的进程并行在这里插入图片描述

进程的状态变迁在这里插入图片描述

  • 运行状态(Running):该时刻进程占用 CPU;
  • 就绪状态(Ready):可运行,由于其他进程处于运行状态而暂时停止运行;
  • 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;

通常会把阻塞状态的进程的物理内存空间换出到硬盘(挂起),等需要再次运行的时候,再从硬盘换入到物理内存,从而避免被阻塞状态的进程占用着物理内存。在这里插入图片描述
挂起的原因

  • 避免被阻塞状态的进程占用着物理内存
  • 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
  • 用户希望挂起一个程序的执行,比如在 Linux 中用 Ctrl+Z 挂起进程;

进程间的切换:
  各个进程之间共享 CPU 资源, CPU 从一个进程切换到另一个进程运行,称为进程的上下文切换。进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器和程序计数器等内核空间的资源。

线程(Thread):进程当中的一条执行流程。

同一个进程内多个线程之间共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。

线程的切换:当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;

线程的实现方式:

  • 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理(用户线程的整个线程管理和调度,操作系统是不直接参与的);用户态线程适用于特定类型的应用场景,例如Web服务器、网络通信、并行计算和事件驱动编程,其中并发性和响应性非常重要。

优点:

  • Thread Control Block 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
  • 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,切换速度快;

缺点:

  • 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。
  • 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
  • 由于时间片分配给进程,在多线程执行时,每个用户态线程得到的时间片较少,执行会比较慢;
  • 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;内核线程适用于需要进行底层系统操作或与硬件直接交互的任务,例如驱动程序开发。

优点:

  • 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
  • 时间分配机制,多线程的进程获得更多的 CPU 运行时间,进而内核线程获得的CPU时间也多,执行更快;

缺点:

  • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如process control block,PCB和 TCB;
  • 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;
  • 轻量级进程(LightWeight Process):在内核中支持用户线程;一个进程可有一个或多个 LWP,每个 LWP 跟内核线程一对一映射,又称为用户态线程或协程。LWP 是由内核管理并像普通进程一样被调度。和用户态线程区别就是和内核态线程映射关系一定是一一映射。

1.1 一个n核服务器最多可以创建多少个进程?

Linux系统软限制ulimit -u,运行后获得:
即机器上最多可以有 65535 = 2 16 − 1 65535=2^{16}-1 65535=2161个进程,但是可以通过ulimit -u 5120改变这个参数的值来修改对于进程数量的软限制为5120。

LINUX中进程的最大理论数计算
段寄存器作用于Global Descriptor Table的位段宽度是13位,每个进程都要在全局段描述表GDT中占据两个表项:一个表项指向这个段的起始地址,并说明该段的长度以及其他一些参数;一个TSS结构(任务状态段)。

所以理论上的最大进程数为 ( 2 13 − 12 个系统开销字段) / 2 = 4090 (2^{13}-12个系统开销字段)/2=4090 21312个系统开销字段)/2=4090

同时进行的进程数
单核CPU在同一时间只能处理一个进程。多核CPU可以同时执行多个进程,进程个数不高于核数。

1.2 一个进程最多可以创建多少个线程?

创建数量受限于进程的虚拟内存空间上限和系统参数限制
ulimit -a 查看进程创建线程时服务器默认分配的栈空间大小,加入为8M,那么32位机就是3G/8M差不多 300 个。64位是128T/8M,但是受限于以下内核参数等:

  • /proc/sys/kernel/threads-max,表示系统支持的最大线程数,默认值是 14553;
  • /proc/sys/kernel/pid_max,表示系统全局的 PID 号数值的限制,每一个进程或线程都有 ID,ID 的值超过这个数,进程或线程就会创建失败,默认值是 32768;

多核CPU的同时多线程技术(simultaneous multithreading)和 超线程技术(hyper–threading/HT)
每个核最多同时运行两个线程。

1.3 程序运行时线程崩溃会导致进程崩溃吗?

  当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃 (这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃) 。所以C++游戏服务器开发时对于游戏的用户设计,则不应该使用多线程的方式,否则一个用户挂了,会影响其他同个进程的线程。

1.4 在main中启动个线程,main执行完了,线程会继续执行吗?

注意:一个良好的程序,主线程要等待子线程执行完,正常退出。
C++:取决于线程的启动方式,join会阻塞主线程,等待子线程执行完毕,然后主子线程回合。detach会分离主子线程,主线程不与子线程汇合,detach后,线程对象将与主线程失联,驻留在后台运行(被C++运行时库接管),可能导致主线程已经退出了,子线程还没执行完的问题。
Java:main方法执行完以后子线程并不会退出。想让子线程在main方法结束后退出,需要将子线程设置为守护线程(守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出),此时子线程退出的时候有一个反应的时间,CPU调度需要时间,所以会打印几个子线程的信息,然后在退出。

2.进程间通信方式

在这里插入图片描述
  进程间,用户空间资源都不共享,但能走内核空间,实现进程间通信。

2.1 管道通信机制

管道通信方式效率低,不适合进程间频繁交换数据。

  管道本质是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。
案例一: 匿名管道(用完后自动销毁
命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入:

$ ps auxf | grep mysql

案例二: 命名管道(Named Pipe)
以Linux系统上的C++进程举例,实现一个管道,模拟两个没有亲缘的进程在该管道中进行数据的读取操作。

fifo.cpp
#include <iostream>
#include <sys/stat.h>
using namespace std;

int main(){
    string name;
    cin >> name;
    mkfifo(name.c_str(),0666);
}

mkfifo(name.c_str(), 0666);:这是核心部分。它调用了 mkfifo 函数来创建一个命名管道。name.c_str() 将 name 字符串转换为C风格的字符串,以便传递给 mkfifo 函数。第二个参数 0666 是创建的管道的权限,表示允许所有用户都有读和写的权限。

read.cpp
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

int main(){
    string file;
    cin >> file;  # 声明了一个名为 file 的字符串变量,用于存储用户输入的管道文件的路径。
    cout << "before open" << endl;
    int fd = open(file.c_str(),O_RDONLY);  # 这行代码尝试打开用户指定的管道文件。open 函数的第一个参数是文件路径(通过 file.c_str() 转换为C字符串)
    if(-1 == fd){  # 用于检查文件是否成功打开。
        perror("open pipe error");
        return 1;
    }
    cout << "after open" << endl;
    char buff[30] = {'\0'};
    read(fd, buff, sizeof(buff));  # 这行代码从已打开的管道文件 fd 中读取数据,将读取的数据存储在 buff 数组中。sizeof(buff) 用于指定要读取的最大字节数。
    cout << buff << endl;
    close(fd);
}

write.cpp
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main(){
    string file;
    cin >> file;
    cout << "before open" << endl;
    int fd = open(file.c_str(),O_WRONLY);
    if(-1 == fd){
        perror("open pipe error");
        return 1;
    }
    cout << "after open" << endl;
    string s;
    cin >> s;
    write(fd,s.c_str(),s.size()+1);
    close(fd);
}

2.2 消息队列

缺点:一是通信不及时,二是附件也有大小限制不适合比较大数据的传输。三是存在用户态与内核态之间的数据拷贝开销

  消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元——消息体,消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

案例:主进程读队列,子进程写队列

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


struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[50];    /* message data */
};

int main(){
    //IPC_PRIVATE
    int id = msgget(0x121212,O_CREAT|O_RDWR|0644);
    if(-1 == id){
        perror("msgget error");
        return 1;
    }


    pid_t pid = fork();


    if (pid < 0)//如果返回值小于0,则创建失败
    {
        printf(" error\n");
        exit(0);
    }


    if(pid == 0){ //返回值等于0,表示创建成功,是子进程运行

        printf("子进程写队列 pid=%d \n",getpid());

        for (int i = 0; i < 100; ++i) {
            sleep(1);

            char text[50] = "x-is sub test";
            int buf_size = sizeof(text)+sizeof(long);
            struct msgbuf* buf = (msgbuf *)malloc(buf_size);

            buf->mtype= i;
            strcpy(buf->mtext,text);

            int snd_ret = msgsnd(id,buf,buf_size,0);

//            printf("snd %s,snd_ret=%d \n",str,snd_ret);
            free(buf);


        }



    }else{ //main
        printf("主进程读取队列 pid=%d \n",getpid());

//		sleep(1);

        for (int i = 0; i < 100; ++i) {

            char text[50];
            int buf_size = sizeof(text)+sizeof(long);

//            struct msgbuf_t* buf_t;
//            buf_t= (msgbuf_t*)malloc(8);

            struct msgbuf* buf = (msgbuf *)malloc(buf_size);
            bzero(buf,buf_size);

            int rcv_ret = msgrcv(id,buf,buf_size,0,0);
            if(rcv_ret>=0){
                printf("rcv rcv_ret=%d,mtype=%ld,mtext=%s\n",rcv_ret,buf->mtype,buf->mtext);
                free(buf);
                buf = nullptr;
            }else{
                printf("rcv rcv_ret=%d \n",rcv_ret);
                break;
            }

        }

    }
    int ctl_ret = msgctl(id,IPC_RMID, nullptr);
    printf("pid=%d,ctl_ret=%d \n",getpid(),ctl_ret);

}

2.3 共享内存

  1. 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程, 所以效率和前面几个相比是最快的,可以用来进行批量数据的传输,比如图片。
  2. 没有同步机制,需要借助 Linux 提供其他工具来进行同步,通常使用信号量。

  共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。

使用共享内存的步骤:

  1. 调用 shmget()创建共享内存段 id,
  2. 调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间,
  3. 访问加入到进程的那部分映射后地址空间,可用 IO 操作读写

案例参考

2.3.1 信号量

  信号量本质是一个整数指示信号,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。如果多个进程同时修改同一个共享内存,需要信号量来指示并保护访问。

信号量是多线程同步用的,一个线程完成了某一个动作就通过信号告诉别的线程,别的线程再进行某些动作。锁是多线程互斥用的

2.4 信号

信号是进程间通信机制中唯一的异步通信机制(可以在任何时候发送信号给某一进程),信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。

一旦有信号产生,用户进程对信号的响应方式有:

1.执行默认操作。Linux 对每种信号都规定了默认操作,例如 SIGTERM 信号是终止进程。

2.捕捉信号。可以为信号定义一个信号处理函数。当信号发生时就执行相应的信号处理函数。

3.忽略信号。当不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

2.5 socket通信

Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

本质都是数据从网卡进入内核。

跨主机进程间通信:
在这里插入图片描述

在这里插入图片描述
  服务端调用 accept 时,连接成功了额外返回一个已完成连接的 socket,后续用来传输数据。监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。

同主机上进程间通信:
  本地字节流socket(local stream socket)和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件

这种类型的socket通常基于Unix域套接字(Unix Domain Socket)。它们在文件系统中绑定到一个本地文件路径上,而不是IP地址和端口。本地字节流socket提供类似于TCP的可靠的、面向连接的通信方式,通常用于在同一台计算机上的进程间通信。在这种情况下,客户端连接和服务器端accept的过程不涉及TCP的三次握手,而是直接建立连接,因为它们共享相同的文件系统。

3.线程冲突及其解决方式

为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:

  • 锁:加锁、解锁操作;
  • 信号量:P、V 操作;PV操作是根据"Produce"(生产)和"Verbruik"(消耗)这两个单词的首字母缩写而来的。

4.乐观锁悲观锁

在这里插入图片描述

  • 互斥锁「独占锁」加锁失败后,线程会释放 CPU ,给其他线程;

    两次线程上下文切换的成本
    在这里插入图片描述

  • 自旋锁[一直自旋,利用 CPU 周期,直到锁可用]加锁失败后,线程会忙等待,直到它拿到锁;

  • 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

  • 乐观锁【先斩后奏,奏失败了白干】:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值