精华内容
下载资源
问答
  • 从事基础测量仪器的市场推广工作十多年来,我遇到了众多的工程师,也无数次共同探讨测试相关的技术问题。但有意思的是,工程师们最关心最多的实际上...他们经常会经历一些困惑,如测量的误差到底是多少、数字表测...

    从事基础测量仪器的市场推广工作十多年来,我遇到了众多的工程师,也无数次共同探讨测试相关的技术问题。但有意思的是,工程师们最关心最多的实际上就是一些基础的问题,毕竟绝他们的主业不是测试测量技术。因此,我最近陆续写了几篇关于时间和频率测量的文章,大家反应还不错。这也给了我写更多高质量文章的动力。

    工程师们最常问的问题是关于精确的直流和交流测量的。他们经常会经历一些困惑,如测量的误差到底是多少、数字表测量显示为什么 不稳定、不同的数字表测量结果为什么差别很大、交流有效值测量结果不可信等等。就此我会写一系列的文章,和大家一起讨论这些问题。在文章中,我会以安捷伦 的34401A和34410A这两款高性能数字万用表为例。34401A是HP公司在1993年的产品,至今仍然是全球销售量最大的6位半数字表,中国有 近10万台的拥有量。34410A是第一款LXI标准的数字万用表。
    首先介绍高精度数字万用表的工作原理。6位半的数字表有着非常高的精 度和分辨率。例如,如果测量5VDC, 其分辨率可以达到1uV。在读数的时候,我们希望是只有最后一位有跳动。如果在倒数第二位,甚至倒数第三位跳动,也就是6位数字显示中只有3位或4位稳定 的,这时候的6位半表也就变成5位半甚至4位半了。那么是什么原因造成了测量结果不稳定呢?
    如果输入的5VDC偏置是稳定的,造成很大测量不确定度的原因首先是噪声。通常情况下,噪声有两种,即串模噪声和共模噪声。
    串模噪声是存在于被测件回路中的噪声, 如下图所示:

    串模噪声的来源是多方面的,例如电源、被测件本身、空间中电磁电磁噪声、还有50Hz 的供电线路公频噪声。对于5VDC的信号,通常会有从几毫伏到几十毫伏峰峰值的纹波加噪声。信号看上去就像下图。 在50Hz 的工频噪声上夹杂着其他的高频噪声。

    因此,为了得到精确的测量结果,首先要考虑到对串模噪声的抑制。以下是数字万用表的工作原理图。

    被 测信号从前端输入。 实际上,对于绝大多数仪器来说,前端是最值钱的部分, 也是衡量不同厂家仪器水平的最重要的标志。信号通过前端的调理, 转换成适合ADC的信号幅度。 图示中的AC RMS 是一个专用的电路,它的作用是对输入交流信号进行有效值的运算。在最新的数字表中,如34410A,这个电路已经不存在了。
    与示波器不一 样,高精度数字表中采用的是双积分式ADC。这种ADC的特点是分辨率极高,对噪声的抑制能力超强,适合于高分辨率、高精度的测量,但速度比较低。例 如,6位半数字表采用的是22bit的ADC,8位半是28bit 的ADC. 数字表内部的工作原理如下图所示

    Vi 是经过前端调理后的被测电压,Vref 是内部参考电源。首先是开关(红色表示)切合到Vi端, Vi对积分器中的电容进行充电。充电的时间是公频周期的整倍数, 即20ms和其整倍数, 目的是抑制工频噪声(如下图)。充电结束后,电容上的电压即等于Vi的平均值。 这时将开关切合到Vref 上,在Vref 的控制下,电容进行固定斜率的放电。同时,用内部计数器记录放电时间。Vi 就可以利用放电时间和斜率算得了。在这个过程中,电容的充电再放电的过程,就可以消除高频噪声。而对50Hz工频噪声的抑制方式如下图所示:

    如果充电的时间在20ms (一个工频周期,即1PLC)或其整倍数的时候,就可以抑制公频噪声。因此,对高精度测量来说,20ms的时间是必须的。当然,如果测量时间越长,例如 10PLC, 当然会获得更高的噪声抑制比。但这样会影响到测量的速度,特别是在高精度数据采集或自动化测试系统中。所以,测试速度和精度实际上是一对矛盾。在实际使用 过程中,要折中考虑。
    不同的数字万用表在同样的测量时间内,对公频噪声的抑制比有区别的。例如传统的34401A, 如果选择200ms 的测量时间,对工频抑制比是60dB. 而对于34410A 新款的产品,在40ms的测量时间,工频抑制比就可以达到120dB。 有些工程师如果从二手市场上买的一些从美国舶来的旧货,就可能有问题,因为美国是60Hz工频周期。

    如果供电工频周期出现不稳定,也会降低公频噪声抑制比。如下图是34410A的公频噪声抑制比和电网频率的关系。 从图中可以看出,如果工频周期偏差了1Hz, 工频噪声抑制比就会下降60dB

    以上重点讨论的是数字万用表结构和对串模噪声的抑制。可以看出,为了确保读数的稳定性和可重复性,我们要考虑到降低并抑制输入的噪声,根据测量速度和精度的要求合理设置测量时间,并选择合适的数字万用表。
    文章知识内容来源于电子工程专辑,如果侵权请联系小编。

    关于云创硬见
    云创硬见是国内最具特色的电子工程师社区,融合了行业资讯、社群互动、培训学习、活动交流、设计与制造分包等服务,以开放式硬件创新技术交流和培训服务为核心,连接了超过30万工程师和产业链上下游企业,聚焦电子行业的科技创新,聚合最值得关注的产业链资源, 致力于为百万工程师和创新创业型企业打造一站式公共设计与制造服务平台。

    展开全文
  • 字符串的格式为:十二斤,五十千克等,是数量+单位形式 替换完为:12斤,50千克等 不用考虑小数,但是要考虑零 C币不多了,求各位大神帮忙
  • 我们可以将识别手写数字这个问题划分为两个子问题,,我们需要将幅包含了许多数字的图像分解为系列独立的图像,每幅图像包含了数字。比如,我们需要把下图分解: 将该图分解为6幅独立的图像: 我们...

    一个简单的手写数字分类网络

    接上一篇文章,我们定义了神经网络,现在我们开始手写体的识别。我们可以将识别手写数字这个问题划分为两个子问题,一,我们需要将一幅包含了许多数字的图像分解为一系列独立的图像,每一幅图像包含了一个数字。比如,我们需要把下图分解:
    这里写图片描述
    将该图分解为6幅独立的图像:
    这里写图片描述

    我们人可以很轻松的将其分开,但是计算机可不那么认为。一旦图片被分割后,程序需要将每个数字单独识别。因此,举个例子,我们想要我们的程序可以识别上图中的第一个数字为5。
    这里写图片描述

    这里我们致力于解决第二个问题,就是对单一数字的分类。因为对于第一个分割问题,这里有许多方法可以解决了。因此,与其关⼼分割问题,我们不如把精⼒集中在设计⼀个神经⽹络来解决更有趣、更困难的问题,即⼿写数字的识别。

    为了识别单个数字,我们将用到三层神经网络:
    这里写图片描述

    网络的输入层包含了对输入像素值编码的神经元。正如后面讨论的,我们的训练数据有许多28X28的手写数字的像素图组成,也就是说我们的输入层包含了28X28=784个神经元,一个神经元对应一个像素位的值。输入的像素值为灰度值,0.0表示白色,1.0表示黑色,0到1之间的值表示不同程度的灰色。

    网络的第二层为隐藏层,我们记隐藏层的神经元数量为n,我们将对n的取值进行实验。这个图例中展示了一个小的隐藏层,它的隐藏层只有15个。

    网络的输出层包含了10个神经元。如果第一个神经元被激活,也就是输出值无限接近1,那么我们可以认为这个网络识别出来的数字是0.如果是第二个神经元激活,那么识别出来的数字为1。更准确的说,我们将对输出神经元从0到9编号,然后找出激活值最高的神经元。如果编号为6的神经元激活值最高,那么我们认为输入的数字为6。

    这里使用是个输出神经元很现实是为了对应输出的十种可能性。一种看起来更自然的方法是使用4个输出神经元,每一个神经元看作一个二进制数,结果取决于改神经元输出更靠近0还是更靠近1。4个输出神经元对于10个数字来说已经足够了,毕竟 24=16 是大于10的。那为什么我们选择使用是个输出的神经元而不是看起来更方便简洁的4个神经元呢?经验告诉我们:通过对两种设计的实验对比,是个输出的神经元比4个输出的神经元更加准确的识别数字。但这是为什么呢?有没有什么启发性的方法可以告诉提前告诉我们使用10个输出神经元比使用4个效果要更好。

    为了理解为什么要这样做,了解神经网络的工作的基本原理是很有帮助的。首先考虑我们使用10个输出神经的情况。让我们把目光放在第一个输出神经上,该神经决定了是否该数字是0。它是通过权衡隐藏层所输出的信息做出判断的。那么隐藏层的神经元做了些什么呢?假设隐藏层的第一个神经元是为了检测图像中是否存在如下图形:
    这里写图片描述

    它是通过对输入图像中与该图形重合的像素赋予很大的权值,而其他部分赋予很小的权值来判断的。以同样的方式,我们假设第二个第三个第四个神经元分别是为了探测图像中是否存在以下图形:
    这里写图片描述

    可能你已经看出来上面四个图形可以组成数字0:
    这里写图片描述

    因此,如果隐藏层的这四个神经元都被激活,那么我们可以认为这个图像上的数字是0。当然这不是我们用来推断数字为0的唯一组合,我们可以通过诸多图形组合得到0(比如将上图进行扭曲变形,但它还是0)。但至少在这个例子中,我们可以推断出输入数字是0。

    如果神经网络是这样工作的,那么我们似乎可以给出一个对为什么使用10个输出神经而不是4个这件事一个合理的解释。如果是4个输出神经元,那么,我们的第一个输出神经元将用于判断该数字对应二进制的最高有效位是什么,然而,我们很难将图形和数字的最高有效位对应起来。

    上面说了这么多都只是一个启发性的方法,没人说三层神经网络必须按照我上面所说的方式去工作,即每个隐藏层的神经元去探测一个简单的图像组成部分。也许一些聪明的学习算法会找到一些权重分配让我们使用4个输出神经元。但是作为一种启发,我所描述的方法可以工作的很好,这可以让你节省许多时间在设计神经网络的结构上。

    使用梯度下降算法进行学习

    现在我们已经设计出了一个神经网络,那么我们如何使用它去学习识别数字呢。首先,我们需要一个数据集去学习,我们使用的是MNIST数据集,它包含了数以万计的手写数字的扫描图像,以及他们的正确分类。下图是取值MNIST的几个图像:
    这里写图片描述

    正如你所见,这些数字和上一篇文章的数字是一样,当然我们需要我们的网络可以识别不在数据集中的图像。

    MNIST数据集包含两个部分,第一个部分包含了60000张图片被用作训练数据。这些通过扫面250个人的手写数字获得,其中有些是美国人口普查局的员工,有些是高中生。这些图像是28X28的灰度图。数据集的第二部分是10000张图片被用来测试,与训练数据的格式一样。我们将使用测试数据衡量我们的神经网络学习得怎么样。当然第二部分的图片是另外250个人的手写样本,这有可以让我们证实网络可以识别不在训练集中的人的手写数字。

    我们用 x 表示一个训练输入。显然x是一个28X28=784的向量,向量中每一个元素表示图像中的一个灰度值。我们用 y=y(x) 表示对应的期望输出值,其中 y 是一个10维的向量。比如,有一个特定的显示为6的图像输入为x,那么它期望的输出值应该为 y(x)=(0,0,0,0,0,0,1,0,0,0)T ,其中 T 表示矩阵的转置。

    我们想有这样一个算法,它可以让我们找到权重和偏差,这样网络的输出y(x)可以拟合所有的输入 x 。为了量化我们如何实现这个目标,我们定义一个代价函数:
    这里写图片描述

    其中w记为网络中的所有权重的集合, b 为所有的偏差,n是所有训练输入的数量, a 是当输入向量为x时网络的输出向量,并且对所有的输入 x 进行计算并求和。的确,输出a依赖于输入 xwb ,但是为了简介,我并没用使用 axi 这样的符号。符号 ||v|| 表示的是向量 v 的长度(模)。我们把C称为二次代价函数,有时也被称为均方误差或者MSE。观察二次代价函数,我们可以发现 C(w,b) 是非负的。此外,对于所有的 x ,当y(x)接近于 a 时,C(w,b)的值是很小的,也就是 C(w,b)0 ,因此我们的训练算法需要能够找到合适的权重和偏差,使得 C(w,b)0 相反,如果 C(w,b) 的值很大,就说明有大量的 y(x) a 相差很大,这是不好的。因此我们的训练算法目标是要能够找到最小化代价函数C(w,b)的权重和偏差。换句话说,我们需要找到一组权重和偏差,使得代价最小,下面我们将使用梯度下降算法来达到目标。

    为什么要介绍二次代价函数?我们不应该关注于图像的正确分类的数量上面吗?为什么不尝试直接最大化正确分类的数量而是要使用最小化一个二次代价函数来简介评测呢?这是因为正确分类图像的数量对于权重和偏差来说不是一个平滑的函数,那么在大多数情况下,对权重和偏差的微小改变不会造成目标函数即正确分类数量的值的改变,这让我们很难找到通过改变权重和偏差提高性能的方法。如果我们使用一个像二次代价函数这样的平滑函数,那将很容易找到通过改变权重和偏差来提高性能的方法。这就是我们为什么专注于最小化二次代价,因为只有这样,我们才能测试分类的准确性。

    那么可能你又会好奇为什么要选择式(6)那样的二次函数来作为平滑函数呢?这时临时想出来的么?可能如果我们选择另外一个不一样的代价函数,我们将会得到不同的代价最小时的权重和偏差。这个疑问是有必要的,稍后我将对上面的成本函数再次进行讨论,并作一些修改。然而式(6)中的代价函数对于我们理解神经网络的基础很有帮助,所以我们将一直使用它。

    重申一次,我们的目标是训练一个神经网络,找到权重和偏差使得二次代价函数 C(w,x) 最小化。这个目标没毛病,但是,现在有许多让我们分散精力的东西——对权重 w 和偏差b的解释,难以弄清的 σ 函数,网络架构的选择,MNIST等等。其实我们可以通过忽略上述这些中的绝大部分,仅仅考虑最小化这一点来理解这些东西。现在,我们打算忘掉代价函数的具体形式,神经网络的组成等等。现在我们只专注于最小化一个给定的多元函数。我们将学习一种用于最小化问题的梯度下降算法,然后再回到我们想要最小化神经网络的特定函数上去。

    OK,现在假设我们尝试最小化某些函数, C(v) ,它可能是任意的多元实数函数, v=v1,v2,... 。注意到我将使用 v 代替前面的w b ,因为我们刚说过我们不再特定研究神经网络代价函数的最小化问题上,而是任意的函数。我们先假设C是只有两个变量的函数,我们称为 v1 v2
    这里写图片描述

    我们想要找到使 C 达到全局最小的位置。现在对于上述的图像中,我们可以很容易的找到全局最小值。从某种意义上说,我可能展示了一个过于简单的函数。通常函数C是一个拥有许多变量的复杂的函数,并且将不可能通过肉眼一下找到它的全局最小值。

    解决这个问题的一种方法是使用微积分的方法。我们将计算函数 C 的导数来寻找它的极值点。运气好的话函数可能只有一个或者几个变量,但是一旦变量过多那将很麻烦。尤其是神经网络中,往往拥有数亿计的权重和偏差,微积分的方法将不再有效。

    现在微积分是不能解决最小值的问题了。幸运的是,有一种漂亮的推导法暗示有一种算法可以很好的解决问题。首先我们把函数想象成山谷,向上面那幅图画一样。然后我们假设有一个球从山谷上沿斜坡滚下,常识告诉我们这颗球会滚落到山谷底部。也许我们可以使用这样一个想法去寻找函数的最小值。开始我们把球随机放置在一个位置,然后模拟球从该点滚落到谷底这一过程。我们可以通过计算C的导数(和一些二阶导数)来简单模拟——这些导数将告诉我们这个山谷的一切局部形状,然后我们就知道这个球该怎么滚落了。

    说了这么多,你可能会以为接下来我将介绍牛顿定理,摩擦力和重力对球体的影响。事实上,我们只是做了一个假设,并不是真的要用这个球的运动来寻找最小值。提到球只是用来激发我们的想象力,而不是束缚我们的思维。因此与其陷进物理学⾥凌乱的细节,不如我们就这样问⾃⼰:如果我们扮演⼀天的上帝,能够 构造⾃⼰的物理定律,能够⽀配球体可以如何滚动,那么我们将会采取什么样的运动学定律来 让球体能够总是滚落到⾕底呢?

    为了使这个问题更明确,让我们讨论当我们将球在v1方向上移动 Δv1 ,在 v2 方向上移动 Δv2 。微积分告诉我们 C 的改变量为:
    这里写图片描述

    我们需要找到一种方法,找到一个Δv2 Δv2 ,使得 ΔC 为负值,即球总是在往低处移动。因此,我们需要定义 Δv v 的变化向量,Δv=(Δv1,Δv2)T。我们还需要定义 C 的梯度为偏导数的向量:(Cv1,Cv2)T。我们用 C 表示梯度向量:
    这里写图片描述

    后面我们将会用 CΔv 来重写 ΔC ,在此之前,我想先解释一下令人困惑的梯度这个概念。当我们第一眼看到 C 的时候,可能会尝试想去理解这个符号的意义。那它究竟是什么意思?事实上,你可以把它简单的当作是上述梯度向量的一个记号。也有很多其它的数学上不同视⻆对于 的专业解释(⽐如,作为⼀个微分操作),但我们不需要这些观点。

    有了这些定义,(7)式中的 ΔC 可以被重新写作:
    这里写图片描述

    这个等式帮助我们理解为什么 C 被称为梯度向量: C C v 的变化关联到C的变化,就像我们期望的用梯度来表示。但是,这个等式真的让我们激动的是因为,它可以让我们知道如何选择 Δv 来使得 ΔC 为负值。特别的,假设我们选择:
    这里写图片描述
    其中 η 是一个很小的,正的参数(被称为学习速率)。然后等式(9)就变为 ΔCηΔCΔC=ηC2 。因为 C0 ,这使得 ΔC0 ,也就是说 C 将总是减小。(当然要在方程9的近似约束下)。这个属性正是我们期望的!因此,我们使用方程(10)来定义球在梯度下降算法中的下降”定律”。也即是我们使用方程(10)来计算Δv的值,然后根据下面的值来移动球的位置 v
    这里写图片描述

    然后我们再一次使用这个更新规则,就可以计算它下一次下降的位置。如果我们一直这样做,C将一直减小知道我们希望它到达全局最小值。

    总结一下,我们使用的梯度下降算法的工作就是重复计算梯度 C ,然后沿着相反方向移动,滚下山谷。我们可以可视化这一过程:
    这里写图片描述

    注意到梯度下降规则并不满足真实的物理规则。在真实世界中,球有动量,动量可能允许它偏移斜坡,甚至向上滚。只有在摩擦力的影响下它才可能滚到山谷。相比之下,我们选择 Δv 规则就像在说”只能往下滚!”,这是一个好的方法去寻找最小值。

    为了使梯度下降正确地工作,我们需要选择一个足够小的学习速率 η ,使得等式(9)可以很好的近似。如果不这样,我们可能将会以 ΔC>0 结束,这显然是不好的。同样,我们又不希望 η 太小,这样会使的 Δv 变化太小,梯度下降算法就会下降得特别慢。在实际实现中, η 通常是变化的,这使得等式(9)能够保持很好的近似度,同时算法又不会太慢。稍后我们将看到它如何工作。

    我已经解释过当 C 只有两个变量时的梯度下降算法了。但是,事实上,即使C有许多变量,也是这么一回事。假设 C 是一个有m个变量的函数 v1,v2...,vm ,然后当 Δv=(Δv1,...,Δvm)T C C的变化量 ΔC 为:
    这里写图片描述

    其中梯度 C 为向量:
    这里写图片描述

    和前面的两个变量时的情况一样,我们可以选择:
    这里写图片描述

    我们保证我们的近似表达式(12)的值也将是负值。这将是我们可以在 C 为多个变量时,通过重复下面这一个更新规则,来找到我们的全局最小值:
    这里写图片描述

    你可以认为这个更新规则就是我们的梯度下降算法。它提供了重复改变位置v来使得函数 C 取最小值的方法。这个规则并不总是有效的,它有时候也会出错并使得我们无法通过梯度下降来寻找C的最小值,后面我们将讨论这个问题。但是,实际使用中,梯度下降往往工作的很好,并且发现它是在神经网络中寻找代价函数最小值的好方法,对网络的学习很有用。

    的确,在这种情况下,梯度下降是寻找最小值的最优策略。假设我们尝试移动 Δv ,最大程度的减小 C .这等价于最小化ΔCCΔv。我们限制步长为一个很小的固定值: Δv=ϵ ϵ>0 。换句话说,我们想在步长固定时,找到使得 C 下降得最快的方向。可以证明,使得CΔv最小化的 Δv Δv=ηC ,其中 η=ϵ/C ,它是由步长限制 ΔC=ϵ 所决定的。因此,梯度下降可以看作一种使得 C 下降最快的方向上做微小移动的方法。

    人们已经研究了梯度下降的许多变化形式,包括一些更接近真实的球的物理运动的形式。这种模拟球的形式有许多优点,但是也有一个重大的缺点:它最终必须计算C的二偏导,这将花费巨大的代价。为了理解为什么这么做代价很高,假设我们需要计算所有的二阶偏导 2C/vjvk 。如果我们有百万个变量 vj ,那么我们需要计算数万亿级别(百万的平方)的二阶偏导。那将会花费巨大。说了这么多,有一些技巧可以避免这些问题,寻找梯度下降算法的代替发也是一个很活跃的研究领域。但这本书我们将使用梯度下降法(和变种)来作为我们神经网络学习的主要方法。

    我们如何将梯度下降算法应用到一个神经网络?其思想是,使用梯度下降算法找到权重 wk 和偏差 bl ,使得代价公式(6)的值最小。为了了解这是如何实现的,让我们重申一下梯度下降算法的更新规则,用权重和偏差代替变量 vj 。换句话说,我们的”位置“现在由 wk bl 组成,梯度向量对应的元素变为 C/wk C/bl 。以向量中元素的形式写出梯度下降的更新规则,我们有:
    这里写图片描述

    通过迭代使用这个更新规则,我们可以“滚下山谷”,找到期望的最小的代价函数。换句话说,这个规则可以用来学习神经网络。

    在应用梯度下降时有许多挑战,我们将在后面的深入探讨这个问题。现在,我仅仅关注一个问题,在提出问题之前,我们先回顾一下公式(6)中的二次代价函数。这个代价函数有着 C=1nxCx 的形式,也就是它是遍及每个训练样本代价 Cx=y(x)a22 的均值。在实际中,为了计算梯度 C ,我们需要单独计算每一个输入样本 x 的梯度Cx,然后再求平均 C=1nxCx 。不幸的是,当训练样本过大时,这会使得训练花费的时间很长。

    有一种叫做随机梯度下降的方法可以用来加速学习。这个思想就是通过随机选取小的训练样本来计算 Cx 来近似估计 C 。通过平均这一个小的样本就可以快速的估计出梯度 C ,这有助于帮助我们加速梯度下降,进而更快的学习。更精确地说,随机梯度下降是通过随机选取一个小数量地训练样本 m 作为输入。 我们将这些随机的输入样本记为X1,X2,...,Xm,并把它们称为一个小批量数据。假设提供的样本容量 m 是足够使得Cxj与计算所有得样本 Cx 的值约等,即
    这里写图片描述

    其中第二个求和公式是对所有的训练样本而言。通过上式我们得到:
    这里写图片描述

    也就是我们可以通过计算样本大小为 m< 全部样本的小批量数据的梯度,来估计整体的梯度。为了明确地将其和神经网络联系起来,假设 wk bl 分别为神经网络的权值和偏差。然后随机梯度下降通过随机选取一个训练样本作为输入,然后通过下式训练:
    这里写图片描述

    其中两个求和符号是在随机选取的小批量的训练样本上进行的。然后我们使用另一组随机的训练样本去训练,以此类推,直到我们用完了所有的训练输入,这样被称为一个训练迭代期。然后我们会开始一个新的训练迭代期。

    另外,对于改变代价函数⼤⼩的参数,和⽤于计算权重和偏置的⼩批量数据的更新规则,会有不同的约定。在等式(6)中,我们通过因子 1n 来改变整个代价函数的大小,人们有时候忽略 1n ,对单个训练样本的代价求和而不是平均。这对我们提前不知道训练样本总数的时候特别有效,比如学习数据是实时产生的。同样,在式子(20)(21)中的 1m 也可以舍去。但是需要注意的是,这样会导致学习速率的改变,对不同工作进行对比时,需要注意是求平均还是求和。

    实现我们的数字分类网络

    现在我们准备开始用代码通过随机梯度下降和MNIST训练集实现我们的识别网络。我们将使用python(2.7)来实现,仅仅74行代码!首先我们需要获得MNIST数据集,如果你使用git,你可以通过克隆代码库来获取:
    git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
    如果你不使用git也可以点击这里来获取数据和源码。

    顺便提一下,之前我说MNIST数据集被分成了60000个训练图像和10000个测试图像,那是官方的描述。事实,我们将对这些数据做一些不同的划分。我们将对60000个MNIST训练集分成两部分,其中50000作为训练集,另外10000作为检验集。在这里我们先不会使用检验数据,但是在后面我们将会发现它对于神经网络中一些如学习速率这样的超参数的设置很有用,这些超参数不是学习算法所决定的。景观验证数据不是原始MNIST的规范,但是许多人都这样使用MNIST数据集,并且在神经网络中使用验证数据是很普遍的。从现在开始我们提到的MNIST训练数据指的是50000张训练图像而不是原始的60000张图像。

    除了MNIST数据集以外,我们还需要python中的Numpy包,用于快速线性代数运算。

    在列出一个完整的代码清单之前,让我先解释一下神经网络代码的核心特性。核心是一个Network类,用来表示神经网络,下面是初始化网络Network类的代码:

    class Network(object):
    
        def __init__(self, sizes):
            self.num_layers = len(sizes)
            self.sizes = sizes
            self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
            self.weights = [np.random.randn(y, x) 
                            for x, y in zip(sizes[:-1], sizes[1:])]

    在这个代码中,list对象sizes包含了各层中神经元的数量。比如,你想创建一个第一层有2个神经元,第二层3个神经元,第三次1个神经元的网络,你可以这样创建类对象:

    net = Network([2, 3, 1])

    网络对象中的偏差和权重是初始化为随机值,使用Numpy包中的np.random.randn函数来产生均值为0,标准差为1的⾼斯分布。这个随机初始化也就是我们使用随机梯度下降算法的一个起点。后面我们将讨论其他初始化权重和偏差的方法。注意,Network初始化代码中是假定第一层为输入层的,并且对这些神经元不设置任何偏差,因为偏差仅仅被用于后面层输出的计算。

    另外偏差和权重以Numpy矩阵列表的形式存储。因此,net.weights[1]是一个存储着链接第二层和第三层神经元权重矩阵。由于net.weights[1]写起来很冗长,我们就用 w 表示这个矩阵。也就是wjk表示的是第二次的第k个神经元和第三层的第j个神经元之间的权重。那么第三层神经元的激活向量为:
    这里写图片描述

    我们需要一块一块地来解释这个方程。 a 是第二层神经元的激活向量(输出向量)。为了获得a,我们用 a 乘以权重矩阵w,然后再加上偏差向量 b 。然后对向量wa+b中的每个元素使用 σ 函数。

    有了这些知识,很容易写出从Network计算输出的代码实例,我们从定义sigmoid函数开始:

    def sigmoid(z):
        return 1.0/(1.0+np.exp(-z))

    注意到当参数z是一个向量或者Numpy数组时,Numpy自动的对向量中每一个元素应用sigmoid函数。

    让后我们添加feedforward方法在Network类中,它对于网络给定输入a,返回对应的输出,这个方法是对每一层应用方程(22):

        def feedforward(self, a):
            """Return the output of the network if "a" is input."""
            for b, w in zip(self.biases, self.weights):
                a = sigmoid(np.dot(w, a)+b)
            return a

    的确,我们想要Network对象做的主要事情是学习。我们用SGD函数来实现随机梯度下降算法。下面是代码,有些地方比较神秘,我会在代码后面逐个分析。

    def SGD(self, training_data, epochs, mini_batch_size, eta,
                test_data=None):
            """Train the neural network using mini-batch stochastic
            gradient descent.  The "training_data" is a list of tuples
            "(x, y)" representing the training inputs and the desired
            outputs.  The other non-optional parameters are
            self-explanatory.  If "test_data" is provided then the
            network will be evaluated against the test data after each
            epoch, and partial progress printed out.  This is useful for
            tracking progress, but slows things down substantially."""
            if test_data: n_test = len(test_data)
            n = len(training_data)
            for j in xrange(epochs):
                random.shuffle(training_data)
                mini_batches = [
                    training_data[k:k+mini_batch_size]
                    for k in xrange(0, n, mini_batch_size)]
                for mini_batch in mini_batches:
                    self.update_mini_batch(mini_batch, eta)
                if test_data:
                    print "Epoch {0}: {1} / {2}".format(
                        j, self.evaluate(test_data), n_test)
                else:
                    print "Epoch {0} complete".format(j)

    training_data 是⼀个 (x, y) 元组的列表,表⽰训练输⼊和其对应的期望输出。变量 epochs 和mini_batch_size 正如你预料的 —— 迭代期数量,和采样时的⼩批量数据的⼤⼩。eta 是学习速率,η。如果给出了可选参数 test_data,那么程序会在每个训练器后评估⽹络,并打印出部分进展。这对于追踪进度很有⽤,但相当拖慢执⾏速度。

    update_mini_batch代码如下。在每一次迭代期,先随机排列训练样本,然后将它分成适当大小的小批量数据。这是一个简单的从训练样本随机采样的数据。然后对每一个小批量数据应用一次梯度下降。它仅仅使⽤ mini_batch 中的训练数据,根据单次梯度下降的迭代更新⽹络的权重和偏置:

      def update_mini_batch(self, mini_batch, eta):
            """Update the network's weights and biases by applying
            gradient descent using backpropagation to a single mini batch.
            The "mini_batch" is a list of tuples "(x, y)", and "eta"
            is the learning rate."""
            nabla_b = [np.zeros(b.shape) for b in self.biases]
            nabla_w = [np.zeros(w.shape) for w in self.weights]
            for x, y in mini_batch:
                delta_nabla_b, delta_nabla_w = self.backprop(x, y)
                nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
                nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
            self.weights = [w-(eta/len(mini_batch))*nw 
                            for w, nw in zip(self.weights, nabla_w)]
            self.biases = [b-(eta/len(mini_batch))*nb 
                           for b, nb in zip(self.biases, nabla_b)]

    大部分的工作有下面这行代码完成:

     delta_nabla_b, delta_nabla_w = self.backprop(x, y)

    这行调用了一个称为反向传播的算法,可以快速的计算代价函数的梯度。因此update_mini_batch 的⼯作仅仅是对 mini_batch 中的每⼀个训练样本计算梯度,然后适当地更新 self.weights 和 self.biases。

    我现在不会列出 self.backprop 的代码。我们将在后面学习反向传播是怎样⼯作的,包括self.backprop 的代码。现在,就假设它按照我们要求的⼯作,返回与训练样本 x <script type="math/tex" id="MathJax-Element-183">x</script>相关代价的适当梯度。

    测试:
    这里写图片描述

    首先创建三个py文件,第一个用来读取数据,第二个用来学习,第三个就是调用上面两个文件中的函数。
    源码下载地址:
    手写数字分类网络源码

    可以看到参数选取为:
    这里写图片描述
    即迭代30次,小批量数据大小为3,学习速率为3.0时的准确率如图:
    这里写图片描述 这里写图片描述

    准确率达到了94.67%。

    我们还可以通过自己生成一张手写数字的图片,测试一下结果,下面的图片是我通过画板自己画的:
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

    我们可以通过改变神经网络隐藏层的神经元数量,或者学习速率,或者小批量数据大小来改变识别准确率。这些参数的选取,调试都是神经网络构造过程中重要的步骤。后面将会继续学习,下一篇文章将会讲到上面没有解释的backprop()函数,即反向传播算法。


    参考原文:http://neuralnetworksanddeeplearning.com/chap1.html#a_simple_network_to_classify_handwritten_digits
    以上是作者对原文的翻译和理解,有不对的地方请指正。


    注:转载请注明原文出处:
    作者:CUG_UESTC
    出处:http://blog.csdn.net/qq_31192383/article/details/77198870

    展开全文
  • 如何零开始写一个操作系统?

    万次阅读 多人点赞 2019-08-20 18:17:55
    如何零开始写一个简单的操作系统? 关注问题 写回答 操作系统 编程学习 如何零开始写一个简单的操作系统? 看了这个:零开始写一个简单的操作系统 求指教。 关注者 4,787 被浏览 352,884 关注问题 ...
    
    

    登录加入知乎

    如何从零开始写一个简单的操作系统?

    关注问题

    写回答

    操作系统

    编程学习

    如何从零开始写一个简单的操作系统?

    看了这个:从零开始写一个简单的操作系统 求指教。

    关注者

    4,787

    被浏览

    352,884

    关注问题

    写回答

    ​邀请回答

    ​3 条评论

    ​分享

    37 个回答

    默认排序​

    知乎用户

    知乎用户

    751 人赞同了该回答

    终于可以来回答这道题了……

    一年多前,也就是大一下学期末的时候,我看到这个问题下

    @fleuria

    叔的答案,然后看了 F 叔给的这个链接 基于 Bochs 的操作系统内核实现 ,当然是什么都看不懂,除了惊诧之外也了解了一件事情:一个人写一个简单的操作系统内核是一件非常帅气并且可行的事情。

    于是我开始写了,那时候我的水平大概是:只会做 C 语言的习题,编译的话只知道按 F9,汇编知道常见的指令,另外会一点点的 Win 32 编程,能流畅使用 Windows。

    一开始我找了《30 天自制操作系统》来看,每天看书,然后把从书里把代码打出来,一次一次地编译运行。因为要同时写汇编和 C,所以从那时候起就开始用 vim。

    在啃完了差不多半本书后,开始觉得没意思了……因为觉得作者为了让内容更简单而省略了太多细节。也看了于渊的《Orange‘s 一个操作系统的诞生》,依然没看下去:汇编用得太多了。期间也曾斗胆发邮件给 F叔,然后他推荐了 Bran's Kernel Development Tutorial 这个教程,于是我就从这教程重新开始了: 「30天自制操作系统」 Stop & 「OS67 」 Start

    那时候大概是大二上学期,于是在 github 上又开了一个 repo,一开始在 Windows 下开发,后来又切换到了 Linux 下,因为 Bran's 用的 bootloader 是 Grub,不符合我的初衷,所以就自己写了一个,之后便跟一路教程写,跨过了保护模式这道坎,完成了基本的设备驱动。

    在完成 Bran's 后,我又部分参考了 写一个操作系统内核有多难?大概的内容、步骤是什么? - To浅墨的回答 中推荐的:hurley25/hurlex-doc · GitHub 文档,完成了一些简单的调试函数和库函数,printk 和内存分配。
    事实证明,尽早写好调试函数诸如 panic, assert 和 printk 是非常重要的。 大量地使用有助于你尽快地发现 bug (当然前提是这些函数本身不能有 bug)。

    看完了 hurlex-doc 该看的部分后,很长一段时间了都不知道该干嘛好,模仿 hurlex-doc 里的内核线程切换也一直出错。这一情况一直持续到我开始读 Xv6, a simple Unix-like teaching operating system

    如果你去看知乎关于「自制内核」的问题,你会发现 xv6 被反复地提及并推荐,事实上它非常值得被推荐:这是我读完大部分代码之后真切体会到的。

    之前的 Bran‘s 和 hurlex-doc 的篇幅都比较小,我是在电脑和 kindle 上看完的,xv6 相对来说代码量比较大,有 9000+ 行和一份文档,之后我又找到了这个:ranxian/xv6-chinese · GitHub xv6 文档的中文译版,所以我就去花了十二块钱学校打印店打印了一份中文文档和一份代码。这又是一个正确的决定,让我不必对着电脑就能看代码。

    在之后的时间里,我先读了 xv6 中文件系统相关的部分,然后改写它的代码为我的内核添加了一个 类似 Minix 的文件系统。 然后几乎又照抄了其中了进程调度的部分(做了部分简化),又在原来的代码基础上为添加操作系统的接口,接着写用户程序,过程几乎是「一路顺风」。看 xv6 的那段时间也经常是处于醍醐灌顶的状态。

    最后我终于在差不多一个月前完成了这个简陋的操作系统内核:
    LastAvenger/OS67 · GitHub (没错其实我是来骗 star 的)

    历时一年,一路点亮了不少技能树(虽然都点得不好),这样算是「从零开始写一个简单的操作系统」么? 跟进一步说,有谁不是从零开始的呢? 所以想做的话,现在就开始做好了。

     

    这是被「翻烂」了的 xv6 源代码和中文文档(其实是放书包里被磨烂了)


    「故事」讲完了,接下来说一点经验之谈吧……

     

    * 知乎上总是有人在讨论「做一个玩具编译器和做一个玩具内核何者更有趣」之类的问题,然后总有各种大V 跳出来说内核有多 dirty 而编译器多 clean,事实上除了 CPU 上的几个表因为历史原因长得恶心一点,内核并没有什么特别 dirty 的地方,另外,想做点什么打发时间,不过是两个代码量稍多的入门项目,有什么好纠结的?
    * 写内核的过程中,你会接触到一些这辈子大概只会用到一次的知识,A20 线已经成为历史,日常的编程里面也不需要你懂得 GDT IDT 的结构。但是单凭内核主要部分部分(文件系统,进程,内存)给你带来的知识而言,这点冗余是值得的。
    * 尽早实现调试函数并大量使用,善于利用 bochs 的内置调试器,能省下你不少时间。
    * 有时候觉得书里的做法非常奇怪,你觉得你有更好的做法,一般是你想错了。(当然只是一般)
    * 上面说看 xv6 一路顺风是假的,20% 时间在抄代码,80% 的时间用来调试
    * 对我这种能力一般的人来说,「写内核」只是好听的说法,正确的说法是「抄内核」。当然,就算是抄一个,也算是受益匪浅了。
    * 抄 xv6 的好处在于,即使你的代码出错了,你可以坚信,正确的答案肯定在 xv6 的代码里,或许只是你还没理解透而已,只要不断地看和理解,你就离正确的道路越来越近。

    最后,感谢

    @fleuria

    在微博和邮件里的多次帮助,

    @To浅墨

    的 hurlex-doc 文档,鲜染同学翻译的 xv6 中文文档,

    @郭家华

    完美地解答了我一开始的疑问,让我在内核中得以使用 C 语言。
    在 #archlinuxcn 频道里也得到了很多人的帮助。

    发布于 2015-11-09

    ​赞同 751​​54 条评论

    ​分享

    ​收藏​感谢收起​

    邱永臣

    邱永臣

    什么都懂一点,同时又什么都不懂,这就是我,一个喜剧演员。

    1,430 人赞同了该回答

    大二的时候,老师(中山大学万海)对我们说:“如果有谁能自己写一个内核出来,那么,他平时可以不来听课,也不用做平时作业,做出来还能加分,怎么样,有没有人有兴趣?”

    和老师一番讨价还价之后,我成为全年级几百号人里唯一一个自己写内核/整个学期都不去教室听课/任何作业都不做的那个人(代表着我的身边将没有可以提供参考的人,任何资料都只能自己找)。

    一开始买了《30天自制操作系统》,上面写着需要软盘还有其它的模拟器,我的初衷是写一个可以烧在真机上一按开机键就能跑起来的那种,所以看了几页后就丢开了。后来又找了国人写的一本,也不是特别符合,也丢开了。

    这时我看到了那本教材(俗称绿宝书),约莫800页。之后的两个星期里,我每天泡图书馆,以每小时10页的速度读完了它,在上面乱涂乱画了许多标记。800页的英文书,我从中学到了大量的基本概念(线程进程,内存算法,寻址方式等等)。

    接着我寻思直接从网络上而不是从书上寻找资料,TA师兄给我提供了一个OS Development,我照着上边的例子,写了数以千记的汇编代码,习得了汇编技能。

    此时,我具备基本的概念知识,对程序的语言也已经理解,知道了虚拟机的调试方法,差的,就只有对内核整体是如何协作不太明白。于是我去找来老师用于教学的PintOS,找来MIT那个项目的代码,还有国内一个高校自制的OS(是几个研究生一起写的),仔细研究了一遍,最后开始写代码。

    在那个学期里,我放弃了LOL,一心看代码,写内核,写各种模块,将过程记录在博客上,花了三个月的时间,最终写出一个具备terminal的内核(文件系统没写好,时间不够),可以跑命令,运行函数,管理内存和进程,处理中断。






    如果你想知道具体整个编写的过程是怎样的,可以看看我当时的记录,如下(很长):

    原文:(http://www.ilovecl.com/2015/09/15/os_redleaf/ )







    (一)OS说明

    今后,我就要开始折腾操作系统,有了一点小小干劲。

    我的计划是,先看过一份用于教育目的的系统源码,再去翻找相应的资料(我手头已有绿宝书),在翻资料的同时开始写代码,然后做好移植真机的工作,DONE!
    我也明白,理性很丰满,现实很骨感,这过程不会如同我计划中这般简单和轻松。但是,见难而退可不是我的风格(那样我会被红叶二小姐调戏的),不管如何,我都会,怎么说呢,尽力吧。

    出于课程需求,斯坦福那些人亲自写了一个名为“pintos”的系统。pintos的结构比较简单,分为进程管理、文件系统、用户程序、虚拟内存等几个部分,也正是因为这个原因,我选择pintos作为我的参考蓝本,现在在读它的源码。

    在接下来的几个月时间里,不出意外的话,我会不断的在博客上更新我的进度。

    (三)交叉编译环境

    倘若我们要在ubuntu上编译另外一个完整的OS,交叉编译环境是必不可少的玩意,维基百科有云:

    交叉编译器(英语:Cross compiler)是指一个在某个系统平台下可以产生另一个系统平台的可执行文件的编译器。
    (想起以前,我为了给路由器编译OPENWRT,下载大量源码,愣是编译了几天几夜。那时候的我,真是“可爱”。)
    为了配置好交叉编译环境,我废了好大力气,最后勉强找到了组织。
    编译环境大致分为2部分,binutils和gcc。我先装好gcc-4.9.1,之后下载gcc-4.9.1和binutils-2.25的源代码,似乎gcc版本与binutils版本要对应来着…

    开始编译之前,需要准备全局变量(在命令行中敲入以下命令):

    export PREFIX=”$HOME/opt/cross”
    export TARGET=i686-elf
    export PATH=”$PREFIX/bin:$PATH”
    编译Binutils
    cd $HOME/binutils-2.25
    mkdir build-binutils
    cd build-binutils

    #注意是在源码目录下面新建一个文件夹,然后cd到该文件夹里,然后才配置configure,不这么做的话,嘿嘿..
    ../binutils-x.y.z/configure –target=$TARGET –prefix=”$PREFIX” –with-sysroot –disable-nls –disable-werror
    make
    make install
    –disable-nls 告诉binutils,不要添加本地语言支持

    –with-sysroot 告诉binutils,在交叉编译器中允许sysroot

    编译GCC
    cd $HOME/gcc-4.9.1
    mkdir build-gcc
    cd build-gcc

    #注意是在源码目录下面新建一个文件夹,然后cd到该文件夹里,然后才配置configure,不这么做的话,嘿嘿..
    ../gcc-x.y.z/configure –target=$TARGET –prefix=”$PREFIX” –disable-nls –enable-languages=c,c++ –without-headers
    make all-gcc
    make all-target-libgcc
    make install-gcc
    make install-target-libgcc
    –disable-nls 告诉GCC,不要添加本地语言支持。

    –without-headers 告诉GCC,不要依赖任何本地库,我们必须在自己的OS中实现库。

    –enable-languages 告诉GCC,不要支持除了C、C++之外的语言。

    提醒
    不同机器配置不同,编译速度也不同。

    编译这两个软件,我花了近3个钟,机器配置之低自不必说,说了都是泪。

    如果任何人的任何编译过程出了任何问题,请仔细地、认真地、用心地再看看上面的命令,在你没有弄懂它的原理之前,请不要擅自做任何“改进”(血淋淋、赤裸裸的教训呀)。

    (五)OS模糊框架

    翻完了手头的绿宝书,我才晓得,人都是被逼出来的。

    操作系统的概念都差不多已经知道,接下来,该由“理论态”切换到“实践态”了喔(书还是不能看太多,会中毒的–)。

    对了,从别人推荐的地方弄来了一个框架(曾在android平台写了几万代码,我深深体会到框架的作用),轻松开工吧。

    先说明一下这个框架:Meaty Skeleton,开源示例,内核和用户分离,方便扩展,嗯,没了。

    最近烦杂事情很多,心情,不算愉快也不算低落吧,近来又梦见红叶,不知道又要发生什么,不管。

    (六)内核第一步任务:GDT完成

    天色已晚,又下着雨,我也忘记带伞了,嗯,等会儿再回去好了,这个商城的环境还是蛮好的。

    今天实现了GDT。

    (也不算是实现吧,因为我打算使用纯分页的流氓招数,放弃纯分段或分段分页混合,所以就不太用心于实现GDT,只是浏览INTEL的官网,借用了几个FLAG定义之类的东西,匆匆就写完了GDT)

    下面是记忆:

    使用内嵌式汇编
    分4个段,两个高级的内核分段,两个低级id用户分段
    预留了一个TSS,虽然也不打算用硬件实现任务切换(听前辈们说,硬件实现非常的麻烦)
    把 设置GDT表的函数(init_gdt)放在kernel/arch/i386/global_descriptor_table.c中,而段 segment_descriptor的定义(seg_desc)则放在kernel/include/kernel /global_descriptor_table.h
    引用了英特尔的一份公开资料
    一些全局或者说全世界通用的参数放在kernel/include/kernel/global_parameter.h,有些人更绝,把所有函数的原型放在一个地方,哪怕内核级函数和用户级函数混在一起
    翻了太多资料,头都晕了
    按进度来看,有点紧,也无妨。

    (七)内核第二步任务:IDT完成

    佛说人者,非人者,名人者。
    已经写好IDT的载入,加上之前的GDT载入,就已经完成两个与机器硬件相关的模块(准确的说,应该是给CPU的特定单元载入内容)。不过我并没传说高手那么厉害,高手们一天一个模块,可我近几天连IDT对应的IRC和HANDLE都还没弄。

    在bochs上调试时,分别 键入info gdt 和info idt 1,能看到GDT和IDT的内容。

    今日要点:

    AT&T汇编和寻常的INTEL有些许区别,不过区别不是很大
    GDT和IDT都是固定的表,必须实现,实现方法各异
    之前留下的TSS并非用于切换任务,而是用于保存从“用户态”回到“内核态”时必须使用的跳转地址
    未完待续
    后记,IDT里面的OFFSET并没有得到正确的值,因为IRQ还没设置好,相应的HANDLE还没有弄好
    2015年4月16日01:14:25补充:

    设 置了IDT表中的头32个项,也就是ISR(interrupt service routines),它专门处理诸如“除以0”/“Page Fault”/“Double Fault”等exception,它对exception的处理方式也很简单,或者说根本没有处理,仅仅是打印exception的类型而已。

    我 随便写了一句int a = 1/0,调试的时候,bochs提示”write_virtual_checks(): no write access to seg”。可能是内核还没具有从用户态跳转到内核态的能力吧,毕竟IDT的头32个项都拥有ring0的级别,明天再看看。

    补上3种中断类型:

    Exception: These are generated internally by the CPU and used to alert the running kernel of an event or situation which requires its attention. On x86 CPUs, these include exception conditions such as Double Fault, Page Fault, General Protection Fault, etc.
    Interrupt Request (IRQ) or Hardware Interrupt: This type of interrupt is generated externally by the chipset, and it is signalled by latching onto the #INTR pin or equivalent signal of the CPU in question. There are two types of IRQs in common use today.IRQ Lines, or Pin-based IRQs: These are typically statically routed on the chipset. Wires or lines run from the devices on the chipset to an IRQ controller which serializes the interrupt requests sent by devices, sending them to the CPU one by one to prevent races. In many cases, an IRQ Controller will send multiple IRQs to the CPU at once, based on the priority of the device. An example of a very well known IRQ Controller is the Intel 8259 controller chain, which is present on all IBM-PC compatible chipsets, chaining two controllers together, each providing 8 input pins for a total of 16 usable IRQ signalling pins on the legacy IBM-PC.
    Message Based Interrupts: These are signalled by writing a value to a memory location reserved for information about the interrupting device, the interrupt itself, and the vectoring information. The device is assigned a location to which it wites either by firmware or by the kernel software. Then, an IRQ is generated by the device using an arbitration protocol specific to the device’s bus. An example of a bus which provides message based interrupt functionality is the PCI Bus.
    Software Interrupt: This is an interrupt signalled by software running on a CPU to indicate that it needs the kernel’s attention. These types of interrupts are generally used forSystem Calls. On x86 CPUs, the instruction which is used to initiate a software interrupt is the “INT” instruction. Since the x86 CPU can use any of the 256 available interrupt vectors for software interrupts, kernels generally choose one. For example, many contemporary unixes use vector 0x80 on the x86 based platforms.
    今天载入到IDT中的,正是第一种类型(Exception),只不过换了个名字叫ISR而已。

    未完待续。

    2015年4月18日12:06:45补充:

    之前的”write_virtual_checks(): no write access to seg”错误并不是权限的问题,而是段寄存器DS的值错误,它的值应该是0x10,可我给它赋值0x08。0x08是段寄存器CS的值,0x10才是段寄存器DS的值。

    另外,这at&t汇编里面,把C语言函数的地址赋给寄存器,必须在函数名前面加上$。

    至此,ISR彻底完成,只是,似乎IRQ又出了点问题….

    未完待续。

    (十)内核第三步任务:分页完成

    稍微做下记录…

    得到内存大小
    首先,利用grab得到物理内存的实际大小。

    物理内存管理
    然后,用一个数组map来监督物理内存,数组的每一项都对应着一个4K的物理内存。在这里我遇到了一个问题:数组的大小如何设置?因为还没有内存分配功能,所以不可能allocate一块或new一块内存来存放数组。找来找去也没找到合适的方案,就自己弄一个粗鲁一点儿的:设置数组大小为1024 1024。这样一来,数组的每一项对应4K,有1024 1024项,恰好可以对应4G大小的物理内存。但这样又有一个缺陷,倘若物理内存没有4G而是128M,那么该数组就有大部分元素被废弃了。现在先,额,不管这个,之后再解决。

    至于这物理内存它的实际分配,我是这么觉得的:把前64M的物理内存当作内核专属(把内核的所有内容全都加载到此处),剩余的物理内存才是空闲内存,用于allocate。

    为了方便分配物理内存,我采取最最最简单的方法:把所有空闲的物理页放到一条链里,需要的时候直接拿出来就可以了。

    虚拟内存管理
    之后,就是把page_directory地址放入CR3并开启硬件分页功能了。

    page_directory,page_table等作用于虚拟地址。对于这4G的虚拟地址空间,排在前面大小为MEM_UPPER的一大块虚拟内存都是内核空间,剩下的排在后面的都是用户空间。也就是说,在有512M的物理的情况下,虚拟内存的前512M是内核态,后面的3584M是用户态。

    分页错误
    内存分配的过程中,可能出现“页面不存在”、“页面只读”及“权限不足”3种错误。处理分页错误,CPU会自动调用14号ISRS,我们要做的,是把我们写的处理函数地址放到14号ISRS的函数栏即可。

    每次分页错误,CUP调用14号ISRS,继而跳入我们设计好的处理函数(-_-陷阱?)。

    不过我现在也是暂时先不写分页错误的处理函数,如果内存真的任性真的出错了,我也不会管它的,傲娇就傲娇吧。

    到这里,分页就算是初步完成了。

    致命的伤痛
    很遗憾,物理内存设置好了,虚拟内存设置好了,也正常工作了,但是我一旦开启硬件的分页功能,就有”physical address not available”的错误,直接重启了,到底是怎么回事…再看看吧…

    未完待续。

    2015年5月1日12:54:14补充:

    bochs的”physical address not available”提示是这么个回事,把一个内容不对的分页目录加载进硬件(也就是把分页目录地址置入CR3)。在初始化分页目录时,我直接用了4M大页的方式初始化,但弄错了byte和KB的数量级,所以就出了一点小小的问题。

    遗留:page fault函数,待日后再写。

    写内存分配去吧!

    未完待续。

    (十一)内核第四步任务:内存分配完成

    内存分配?这可是个麻烦的活,不过,如果你足够聪明的话,就没什么问题了。 ——前人
    上 一次,我准备好了分页的相关内容,比如说,载入分页目录/开启硬件支持/划分物理内存/划分虚拟内存等等。这一次,不会怂,就是干(为写内存分配模块而奋 斗,高扛自由的鲜红旗帜,勇敢地向前冲….)。分页准备好之后,下一步是如何地分配内存,比如,如何分配一页空白的可用的物理内存?如何分配一块空白 的虚拟内存?如何连续地分配等等等等。
    第一节:申请和释放空白物理内存
    申请物理内存,在分页的机制下,就是申请一页或连续几页空白的物理内存,释放则反过来。

    在 分页的时候,我已经将所有的空白物理页都放进了一个链表之中,现在要申请一个空白物理页,从链表中拿出来即可,太简单了。释放空白物理页,将物理页重新放 进链表里即可,也是非常的简单,有点简单过头了。当然啦,简单有省时省力的优点,同时,也有“无法同时分配许多页/分配大内存时(比如数十M)很吃力”的 缺点。这,按我的习惯,先留着,以后再说,现在能简单就简单。

    写好allocate_page和free_page两个函数之后,分配空白页倒是正常,但是内核出现”double fault”的错误,也就是8号ISR被CPU调用了,具体为甚,现在还不清楚,待我瞧瞧再说。

    未完待续。

    查资料如下:

    Normally, when the processor detects an exception while trying to invoke the handler for a prior exception, the two exceptions can be handled serially. If, however, the processor cannot handle them serially, it signals the double-fault exception instead. To determine when two faults are to be signalled as a double fault, the 80386 divides the exceptions into three classes: benign exceptions, contributory exceptions, and page faults. Table 9-3 shows this classification.

    Table 9-4 shows which combinations of exceptions cause a double fault and which do not.

    The processor always pushes an error code onto the stack of the double-fault handler; however, the error code is always zero. The faulting instruction may not be restarted. If any other exception occurs while attempting to invoke the double-fault handler, the processor shuts down.

    ————————————————————————–

    Table 9-3. Double-Fault Detection Classes
    Class ID Description

    1 Debug exceptions
    2 NMI
    3 Breakpoint
    Benign 4 Overflow
    Exceptions 5 Bounds check
    6 Invalid opcode
    7 Coprocessor not available
    16 Coprocessor error

    0 Divide error
    9 Coprocessor Segment Overrun
    Contributory 10 Invalid TSS
    Exceptions 11 Segment not present
    12 Stack exception
    13 General protection

    Page Faults 14 Page fault
    ————————————————————————–

    Table 9-4. Double-Fault Definition
    SECOND EXCEPTION

    Benign Contributory Page
    Exception Exception Fault
    Benign OK OK OK
    Exception

    FIRST Contributory OK DOUBLE OK
    EXCEPTION Exception

    Page
    Fault OK DOUBLE DOUBLE
    ————————————————————————–

    大概意思是:同时出现了2个中断,CPU不知道该处理哪个先,就是这样,就是如此的简单。之前没有这个错误,但分配和释放几个物理页之后就有这个问题,我估摸着两个都是Page fault,再看看吧。
    刚刚调试了一下,我发现不是分配和释放几个物理页的问题,而是cli()和sti()的成对出现,去掉它们就没这个问题;更奇怪的是,就算只有sti() 允许中断出现,也会double fault,莫非我这前面关了中断或者是前面遇到了不可解决的中断遗留到现在?难道,是irq的重定位有问题?到底是为什么呢?先算入历史遗留问题吧,还 有重要的模块要完成。
    (事情有点麻烦了呢?并不是内存分配这里出了问题,而是sti()惹的祸,不管这哪个位置,只要调用sti()开启中断,就会double fault,看来必须解决这个问题才行,我不可能一直不开中断吧…-_-)
    睡了一觉,起来查资料,看到了关键的一句:make sure you didn’t forget the CPU-pushed error code (for exceptions 8,10 and 14 at least)到了,我翻出代码一看,哎呀嘛,我只注意到了8号软中断,没注意到10号和14号软中断(14号处理page fault),删去两行代码后,顺利开启中断!
    未完待续。
    第二节:分配内存(malloc/free)
    既然已经可以正常地分配和释放物理内存页,那么在这一小节之中,很自然地,我的任务就是分配内存了。

    所谓“天将降大任于斯人也,必先让他实现一个内存分配的算法”,不外乎就是说,要实现void malloc(int size)和int free(void p, int num_page)两个大众情人函数。

    它 的大概思路就是这样的:先初始化一个桶,把可用的内存块都塞进去,要分配内存时,直接从桶里面找,找到了当然万事大吉大家都开心,如果找不到,就调用上面 那个申请空白的物理内存页的函数,弄一个4K物理内存页过来,将这个内存页分割成小块,丢到桶里面,然后继续找,就是这样….
    2015年5月5日23:19:08补充:

    遇到一个bug:每次申请的时候,可以正常申请,但是一旦使用了申请的内存,内核就报”page fault”的错误。想来想去,看来看去,最终发现,我在初始化分页机制的时候出了点小小的问题。

    秘技解决:

    初 始化虚拟内存时,我将大小和物理内存一样大(比如129920K)的虚拟内存设为内核级别并可用,剩下3个多G的虚拟内存是用户级别但不可用,我使用4M 大页载入分页表,所以我实际上载入了129920/4096 = 31个大小为4M可用的内核级别虚拟内存页,也就是说,在虚拟内存这个空间里,仅仅有31 4096 = 126976K的可用空间,其它的虚拟内存均是不可用的非法的;而在初始化物理内存时,我将前64M留给内核,后面的物理内存用于malloc和 free,比如有129920K,我把它划分为129920 / 4 = 32480个4K大小的物理内存页,也就是说,在物理内存这个空间里,仅仅有32480 4 = 129920K的可用空间,其它的物理内存均不在管理范围之内;这样一来,就出大问题了。

    假设我们要申请一个物理页,由于使用链的方式管理物理页,申请到的就是排在后面的物理内存,比如申请到了129916K到129920K这一个物理内存页,现在,我们要使用它,会发生什么呢?page fault!!!!!!!

    为 什么?很明显,在虚拟内存的空间里,最大的有效内存是126976K,CPU的分页表里只能找到前126976K,现在让CPU去找129916K,它根 本就找不到!它以为这个虚拟地址并没有对应这物理地址,是个错误!(附上page fault的引发条件:A page fault exception is caused when a process is seeking to access an area of virtual memory that is not mapped to any physical memory, when a write is attempted on a read-only page, when accessing a PTE or PDE with the reserved bit or when permissions are inadequate.)
    于是我稍作改正,就正常了,可以正常使用申请到的内存-_-。

    未完待续。

    (十二)内核第五步任务:系统时钟中断、键盘中断

    我现在的状态不是很好,刚弄好系统时钟中断,每10ms发出一个中断请求;但键盘中断并没有弄好,没有识别键盘的按键SCANCODE,所以暂时只能识别第一次按键,系统收不到第二次按键中断,明个儿我再来看看,已经很晚了--!!
    未完待续。

    2015年5月9日 15:51:00补充:

    查了一番资料,调试了一番,现在,键盘中断正常工作了,键盘可以正常工作,每输入一个字符,就在屏幕上显示出来。

    嗯哼,可以进入到进程模块了。

    (十三)内核第六步任务:进程创建

    在自习室里,我突然想到一个问题:一个进程,如何去创建它?(虽然之前翻完了大宝书,但毕竟一个多月都过去了,忘了具体的实现-_-)

    翻 翻书,找到一个和我的设想相差不多的方案:用一个特定的结构体代表一个进程,结构体中包含进程的相关信息,比如说进程的pid、上下文、已打开的文件、优 先级、已经占用的CPU时间、已经等待的时间、虚拟内存空间、错误码等等,创建进程的时候,只需要跳转到进程的虚拟内存空间即可。至于如何跳转,那就是内 核态的事情了,一般的进程都处在用户态,也就不必关心太多。

    如此,我们便是可以创建并运行一个进程了(不考虑文件系统),既然可以创建进程,可以切换进程,那么进程调度就很容易了,不过就是个复杂的进程切换过程而已,下一节便是写进程的调度罢。

    (十四)内核第七步任务:进程切换与进程调度

    黄粱一梦。
    看到这句古语,顿时感慨万千,没想到仅仅数周时间,我的人生竟发生了这么大的转折(不是一夜暴富),仿佛一夜醒来,到另外一个平行世界里去。甚至,在睡梦中我都会惊醒。

    逝 者已逝,再多的话语也没用。只是,我不甘愿就这么结束而已。她也曾经说过:“此身不得自由,又何谈放纵”,现在我竟是极度赞同了。曾经想过在割腕的那一瞬 间,她的脑海里究竟有什么,有没有浮光掠影,有没有回放这一生的片段?如此年轻的生命,选择自我了断,需要多少黑暗沉淀,多少的落寞与失望…似乎一下 子也看开了。

    (以上只是个人情感的流露,忍不住必须得写些什么,请忽略)

    简单记录一下吧,没什么心情。

    进程切换时,只需要切换进程上下文,把context刷新一遍即可。

    至于进程调度,这个就简单许多了(其实也挺复杂),在时钟中断到来的时候,调整各个进程的优先级,并切换到相应的进程,就是这么简单。

    嗯,就这样吧,现在只想戴上耳机听听音乐….

    编辑于 2016-05-12

    ​赞同 1.4K​​108 条评论

    ​分享

    ​收藏​感谢收起​

    梦人亦冷

    梦人亦冷

    嵌入式/竞赛达人/cs大法好

    492 人赞同了该回答

    我来写一个如何在15天内完成一个嵌入式实时操作系统,并移植到stm32单片机的攻略吧。第一次看到这个问题是在大概两个月之前,从那时候开始决定自己也写一个操作系统,于是开始看os的基本概念,进程切换,任务调度,内存管理,任务通信,文件系统等等。


    前言:
    大约在两个周不到前动手,平均每天7个小时,从完全不知道怎么下手(真的快哭了),到现在完成了一个----基于优先级时间片调度,具有信号量,消息队列,内存管理的嵌入式系统并命名为--LarryOS,并且移植到stm32(Cortex-M3架构)上,还在stm32上实现了一个malloc和free函数,受益匪浅。现在还正在完善。我是用自己命名的,当然,大家写好了也可以用自己命名,图个开心么 ,想想是不是很激动啊。

    对于这个问题下的回答的看法:
    大神真的好多,几乎都是x86型的,难度真的要比我的大,不得不承认。不过,我觉得对于新手,写那样一个系统真的是太艰辛了,比如我这种入ee坑的在校大三学生,课真的太多了,周内最少三节课,而且和cs没有一毛钱关系,课余时间太少,如果花费一个学期或者三个月的时间来写,可能有些得不偿失。因为许多想写os的朋友,和我一样,一是觉得写os很酷,二是想通过写来理解os,毕竟看书看了就忘,也没有衡量自己学的好坏的标准。因此,选择写嵌入式系统,是一个非常好的选择 。

    知识储备:
    这个问题想必是很多同学顾虑的,到底写一个嵌入式系统需要什么知识储备?我从自己的经历出发,并加以简化,大家可以参考一下。

    自身版:
    1c语言要求:我自己在大学里几乎没学过c语言,上机几乎10道题会2道,期末全靠背。后来开始自学了c,看了c语言程序设计现代方法,c和指针 c专家编程 第一本书的课后题,做了3分之2吧,就这样,没了
    2汇编要求:会基本的x86指令,仅仅是基本,看了王爽的汇编语言,程序写的很少,仅仅上机课写过,不过能很快写出来。
    3微机原理或者计算机组成原理:老师上课太坑了实在,我自己看了csapp的前四章大概,豁然开朗,推荐这个。
    4操作系统:看了三个礼拜os的基本概念,仅仅是了解概念,没办法深入,也没地方。。。。
    5数据结构:看过浙大的数据结构公开课的3分之2,会写基本的链表,队列,最基本的树和树的基本的遍历办法,图完全不会
    6单片机基础:大约两年的单片机基础
    7新手看到这里,可能已经慌乱了。。。。不要怕,我说的很多东西,写嵌入式系统用不到啊 ,继续,发一个精简版

    精简版
    1:c语言 能把我推荐的c书的第一本或者c primer之类的书看个一半,或者自己本身知道指针的概念,知道结构体指针,函数指针,二级指针的用法,仅仅是概念和写法就行了,不用深入,用不了多久。
    2汇编:知道有几条常用指令,用法和功能是什么就可以了
    3组成原理:知道中断是什么,入栈和出,寄存器,知道汇编对应的计算机大概动作就可以了,用不了一个周
    4操作系统:找个公开课或者书,看看大概的os概念,一个周就够了,我现在很多概念还是不知道,后面可以慢慢学
    5数据结构:会写链表,队列就行了,我们不写文件系统,不用树
    6单片机基础:用过单片机,不用自己写驱动代码,仅仅可以在别人的驱动下,编写一些简单的逻辑代码就行,完全没学过的人,两个礼拜就可以用stm32来编程了,如果之前学过51,一个礼拜不到即可,因为我们只是借助一下单片机,避免使用虚拟机,方便操作系统的调试。因为我们可以用单片机,输出某些信息在串口或者液晶屏,让我们直接看到代码的错误点,方便我们调试。

    因为很多大一的新生想写,推出一个极限版~~
    极限版
    1.学过C,知道有指针这个东西,其他的边做边学。
    2.不懂汇编,边做边查,边查边写。
    3.知道什么是寄存器,知道中断的概念
    4.知道OS是由什么组成的
    5.数据结构完全不会,遇到链表,队列时再查,或者直接抄也行
    6.不学如何使用单片机,遇到再查


    发一个很装逼的封面助兴



    正文:

    一、开发环境

    对我来言,这倒是很重要的一点。第一次萌生想写OS的想法时,在网上搜索了不少资源,大多数都是在叙述框架,如何构建一个操作系统。然而对于当时的我来说,根本不知道用什么平台来写,如何调试自己的程序,看了一些朋友的发帖,推荐用XX模拟器,在XX平台开发,完全看不懂,界面好像也很老旧,而且大多是英文,当时有点敬而远之。自己手上只有个codeblocks软件,想来想去也不知道怎么用这个开发OS。直到有一天,突然顿悟,OS不就是一堆程序,我还打算让他运行在单片机上,那么用单片机常用的开发工具不就行了!!!---------Keil uVision5

    学过单片机的朋友,相信非常熟悉这个软件,使用起来也非常简单,网上随便一查,就可以下载和安装了,这里就不详细展开说了。如果不知道如何使用的,先熟悉一下这个环境,再开始写,相信半个小时即可上手。


     

    二、参考资料

    既然是运行在真机上,必然要对它有所了解,我们这里采用的是STM32(Cortex-M3架构),市面上非常火热的一款,资料丰富,大家有什么问题谷歌一下,有很多前人的经验让你借鉴。如果不知道如何谷歌的朋友,点开这个链接去操作,免费,大约15分钟就能翻墙了如何优雅的访问谷歌、谷歌学术等网站 | 欧拉的博客

    1.Cortex-M3权威指南(中文版),这本书会详细的讲解,中断处理,异常,ARM汇编等知识,我们会在任务切换的时候用到。(PDF即可)

     

    2.嵌入式实时操作系统ucos(邵贝贝审校),ucos中有很多我们可以借鉴的地方。

     

    3.谷歌,有一些基础知识遗忘的时候,谷歌可以让你很快补充上来

     


     




     

    三、从写一个最简单的os做起

    我们这里假设我们写一个支持32个任务并发执行的简易OS

    1.任务就绪表

    我们假设这里任务有两种状态,就绪态和非就绪态。

    我们定义一个32位的变量OSRdyTbl(就绪表),它的最高位(第31位)为最低优先级,最低位(第0位)为最高优先级。OSSetPrioRdy()函数的功能是,你传递一个数(优先级),把这个优先级对应的任务设置为就绪态。同理,见图:

    OSDelPrioRdy()函数将某任务从就绪表移除

    OSGetHighRdy()函数选出最高优先级的任务

    这里我们就完成了,设置某任务为就绪态,从就绪态删除某任务,获得最高优先级任务的任务。

    2.任务控制块

    想必这个概念大家很清楚了,每个任务都对应一个任务控制块,典型的用处是,任务切换时,通过控制块来获知每个任务的堆栈(因为控制块有指针指向该任务的堆栈)

    此外,再定义几个变量。

    注释写的很清楚,不解释了!

    3.主堆栈

    在此STM32中,提供两个堆栈指针,一个是主堆栈(MSP),一个是任务堆栈(PSP),可以通过查Cortex-M3权威指南(中文版)得到。所以我们既要建立一个主堆栈,又要为每个任务建立自己的堆栈(一个任务一个),这里我们先不管任务堆栈,只看主堆栈。

    OS_EXCEPT_STK_SIZE是个宏,大家可以自己设定,我这里设的是1024,一定要尽量大一些。为什么?因为裸机下,进入中断服务程序时,系统会把许多寄存器入栈,而且支持中断嵌套,也就是刚入栈完又入栈,所以有可能会堆栈溢出,非常危险。

    CPU_ExceptStkBase大家先别管,它指向的是数组最后一个元素。

    4.建立一个任务

    我们通过Task_Create()函数来建立一个任务,第一个参数用来传递该任务的任务控制块,第二个参数用来传递函数指针,第三个传递该任务的堆栈。tcb->StkPtr=p_stk这句将该任务控制块中应当指向栈顶的指针,指向了该任务的新栈顶(前面定义了TCB,自己可以翻一翻),在写该函数时,一定要看Cortex-M3权威指南,不然你怎么知道有这么多寄存器,而不是仅仅从R1到R7?看到这里,还想看懂的,应该都是真的想写OS的朋友,这里有不懂的去看Cortex-M3权威指南,你会豁然开朗的。这里,Task_End是一个几乎永远不会执行的函数,何时会执行,先不管了,看它的内容。

    5.欢迎来到主函数

    ①第一行:在该主函数中,第一行我们让主堆栈指针指向了主堆栈,那么,这个主堆栈是哪里来的呢?很简单,我们自己定义的,哈哈。

    很熟悉吧,前面发过这个图,大小你来指定,注意要大!!!!!

    ②第二、三行:建立一个任务,第一个参数传递了该任务的控制块,第二个参数是该任务的任务函数,第三个是堆栈(数组最后一个)

    是不是很好奇,任务1和任务2是什么?一起了来看

    大家自己随意定义,开心就好。

    Task_Switch()函数又是什么呀?

    这里Task_Switch()是我们用来测试的程序,当任务1运行时,完成i++后,将最高优先级设置为任务2,并用OSCtxSw()切换到任务2,OSCtxSw是用汇编写的,是一个隐藏BOOS,我们先不管。

    ③第四行:程序刚运行时,是没有最高优先级的,所以我们用p_TCBHighRdy=&TCB_Task1;来随意指定一个任务为最高优先级

    ④:最后一行:OSStartHighRdy()该函数也是汇编,和OSCtxSw()并称为2大BOSS,我们会在后面解密。OSStartHighRdy和OSCtxSw很相似,不过OSStartHighRdy()用于当所有任务都没有运行过时,用于初始化(当然具有任务切换的作用)并成功运行第一个任务,而OSCtxSw()是在OSStartHighRdy()之后,使用OSCtxSw()时,最起码有一个任务已经运行了(或正在运行)。

    6.完工?

    到这里,从宏观说已经基本完工了,这就是一个简易的OS的基本状况。看到这里,大家可以休息一下了,消化一下,马上有BOSS要出现了,解决那两个BOSS后,就真正做成了一个简易的OS。

     

    7.两大boss之OSStartHighRdy()函数

    7.1任务视图

    终于开始了任务切换环节,这是我画的一副任务切换图,自我感觉非常好,不过大家应该看起来很困难,字太丑了~~~~

    通过分析上面这张图,来确定如何写OSStartHighRdy()函数:

    ①中:此时有一个任务1,但是任务1我们仅仅是建立了,并没有让它运行。这里我们认为任务1是由三部分组成:任务代码,任务堆栈,该任务的任务控制块。

    ②中:我们想让任务1运行起来,任务1是什么?就是一堆代码,如何运行起来?-----让PC(程序计数器)指向任务代码即可(依靠出栈POP)。同时,我们还要让SP指向任务的堆栈,这里的SP当然是PSP(任务堆栈)

    7.2写OSStartHighRdy()函数

    下面开始写OSStartHighRdy(),它的功能就是上图的①和②

    2,3,4,5行中的那些数字,是外设对应的地址,我们往相应的地址写值,即可完成某些目的。12,14,15行是我们之前定义过的几个变量,忘了的回头翻一翻。17行是将OSStartHighRdy()函数extern了一下,因为我们在主函数要用。

    上图就是OSStartHighRdy的内容,我们一起来看。28,29,30行设置了PendSv异常的优先级,问题来了,什么是PendSv???

    Cortex-M3权威指南中其实讲了,很详细,这里为了缩减篇幅,不详细说,大家只用知道PendSV 异常会自动延迟任务切换的请求,直到其它的中断处理程序都完成了处理后才放行。而我们只用触发PendSV异常,并把任务切换的那些步骤,写在PendSv中断处理任务中。

    7.2.1PendSv处理程序

    经过39和40行,我们往控制器里写值,触发了PendSv异常,现在程序会进入异常处理程序,就是下图:

    在此函数中,如果PSP为0,则进入OS_CPU_PendSVHandler_nosave()函数,其实在OSStartHighRdy我们将PSP设置为了0,所以必然会进入OS_CPU_PendSVHandler_nosave()函数。

    7.2.2OS_CPU_PendSVHandler_nosave()函数

    在该函数中,我们让p_TCB_Cur指向了最高优先级的任务,因为有出栈,所以重新更新了SP。

    7.2.3恭喜

    到了此处,一个任务已经可以成功运行起来了!!!!!!

    BUT only one task!我们需要让它多任务切换

    7.3写任务切换OSCtxSw()函数

    与OSStartHighRdy非常相似,也是往中断控制器里写值,进入PendSv异常。

    7.3.1任务视图

    依然是这张喜闻乐见的图!!!!!!!

    ③:任务1要切换到任务2,因为待会要进入任务2,会破坏任务1,所以我们要保存任务1的现场,把寄存器入栈

    ④:因为任务1有入栈动作,栈顶肯定变了,我们修改任务1控制块的值,让它指向新的栈顶

    ⑤:什么都没有,只是想说明。我们建立了一个任务2,仅仅是建立了。

    ⑥:很简单,要想让任务2运行,则让PC和SP分别指向任务2的任务代码和任务堆栈即可。

    ⑦:什么都没有,只是想说明,任务2活的很开心,可以运行了

    7.3.2由OSCtxSw()进入PendSv异常

    与OSStartHighRdy不同的是,这时的PSP肯定不为0了,所以不会跳转,会顺序执行55行以后的程序,也就是任务视图里的③,继续执行④。执行完60行的程序后,继续顺序执行,执行下面程序:

    和OSStartHighRdy函数的后续步骤几乎一样,找到优先级最高的任务,让指针指向它即可!!!!

    1.大功告成!!!!!!!!

    到这里,我们已经彻底完成了一个简易OS的设计!!!!

    8.1如何查看成果?

    8.1.1增添程序

    我们在主函数中加入一个函数

    大家可以把它理解为一个库,调用之后,我们就可以在串口(屏幕)显示某些字符了。

    同理,在任务1和任务2中加入printf函数

    8.1.2编译并下载程序

    8.1.3利用串口调试助手观察

    网上搜串口调试助手,会有很多工具,随意下一个就OK!

    可以看到,按照我们的预期,任务1和任务2轮流输出字符在屏幕上!

    8.2如何调试程序?

    8.2.1调试器

    必然是神器--J-link或者ST-link,一个大概50,嵌入式开发神器,记得以前刚开始玩单片机的时候,调试全靠打印消息在屏幕,觉得好用的不得了,经常有人给我说,你用调试器调试啊,我都鄙夷的回一句,需要么?(哈哈,那时确实不需要) 等开始写OS时才知道,这东西真是救命稻草,没有它,怎么看寄存器的值和异常返回的值呢?

    8.2.2界面

    点击DEBUG后,你可以看到寄存器的值。想想我们之前要入栈,出栈,如果哪一步错了,自己估计把串口吃了,也看不出来吧,哈哈!!!!!!

    单步调试什么的,不说了,用的很多。

     

    9.写到此处的感想

    答主因为写这个OS,在凳子上久坐了两周,这两天腰疼,只好躺着写这个回答了!哈哈!

    也正好因为腰疼,感觉时间比较多。不过和想象的真的不一样,本来觉得可以一气呵成,结果短短的篇幅,就写的自己的思维都大乱了,而且也挺费时间的,前后用了有6个小时了。想写OS的朋友,参考上面的步骤,加上自己琢磨,应该也能写一个出来。如果哪里有不清楚的,不要心急,如果真的是一两天就写成了,还有什么锻炼的意义,有点失去初衷了。不懂的就去查权威指南和OS的书籍,相信你会收获的非常多!




    为什么要写这个回答?
    这是我写了6个小时多以后来补充的问题,因为我自己也纳闷了,放着自己的事不做,跑来写这么一堆干什么........刚才路上走着想到答案了----------想留个纪念。还有不到两个月就要寒假了,我打算考研,也就是说这是大学里做的最后一个项目了(毕设除外),真的挺伤感。从大一一路折腾过来,现在要突然一年不能折腾,简直泪流满面!!!!!!!!!!!!!!!!!!希望我这个简单的开头,能让想写OS的新人得到一丝启发。

    写操作系统能学到什么?
    这也是我想写这个回答的原因,对我的改变挺大
    前几天有个好朋友(大一开始和我一起学单片机的朋友,后来他一直在做单片机项目,却没有补过任何基础知识),打电话问我XX模块怎么用,其实是很简单的一个问题,直接百度都可以,但是他还是想要来问下我,我说很简单,就XX做就可以,有问题你再百度,但是他好像不想听到这种回答,想让我说到他一听就知道怎么做了,才敢开始做,不然就不敢启动项目。那时我才突然意识到,以前的我就是这样啊,做什么项目做什么东西,老是想要集成的模块,资料丰富的模块,如果没有什么源驱动代码,我就不敢做了,生怕买回来,用不了之类的,甚至有源码也不敢买,因为源码和我的机型不一样,连修改源码都怕。开始写OS,我还蛮恐惧的,因为不是科班,没学过,不懂。涉及的东西也很广,从编程语言到数据结构,组成原理,操作系统,怕拿不下来,也找不到好的资源,不知道怎么写起。通过这次项目,我想应该学到了一下东西:

    1.遇到不懂的没有那种很强的抵触感了,开始学会查芯片手册,查原理书,开始配合谷歌查BUG,现在即使是拿来我没有接触过的模块,无论最后做的出来与否,我肯定先去了解它是什么,概念是什么,再去找厂家提供的资料,提供的源码,去一行一行的看,自己修改或者重写,这个比下面说到的什么具体的知识,我想都重要的多。我现在还记得第一次和第二次,第三次打开Cortex-M3权威指南(中文版)时的情景,看了几页就吓得我关了,简直是天书啊,其实耐心看,真的能看懂。
    2.把自己数据结构里学的东西,真正带到了项目,虽然也写过队列等这些数据结构的题,但是在以前的嵌入式开发里,几乎用不到,有时候觉得好没用啊这些。这次通过实现OS的消息队列,看linux的文件系统,知道了这些在实际的巨大用处。
    3.对OS的基本概念和运转有了认识
    4.对C语言的理解深刻了许多
    5.每个人的体验都不一样,没什么补充的了O(∩_∩)O哈哈~
     

    四、简单几步将简易OS改造为--优雅的简易OS

    1.为什么不优雅?

    在该函数中,任务1执行完,立马就会切换到任务2,然而在实际中,我们希望是这样的:

    任务1每100毫秒执行一次

    2.系统节拍

    几乎每个实时操作系统都需要一个周期性的时钟源,称为时钟节拍或者系统节拍,用来跟踪任务延时和任务等待延时

    我们在main函数中输入这样一句

    这里我们配置了定时器中断,每5毫秒一次中断。中断后会进入中断处理函数,下面来写中断处理函数:

    大家只用管if里面的函数即可:我们在某个函数中将TCB_Task[i].Dly置为x,中断处理函数会每5毫秒,将非0的TCB_Task[i].Dly减一。如果TCB_Task[i].Dly非0,对应的任务是不会运行的(因为被我们删除就绪态了,这里看不到),当TCB_Task[i].Dly减为0,我们才将该任务置为就绪态

    3.编写OSTimeDly()函数

    也就是1中图片所示的函数,它可以让某任务指定每X毫秒运行一次。

    第65行,可以关闭中断,同理,第68行,开启中断。为什么要关闭中断?因为中断会影响我们执行下面的代码,先关闭中断,执行完后再打开。66行将该任务从就绪态变为非就绪态,将要延迟的时间赋值为TCB_Task[OSPrioCur].Dly,然后调度(也就是切换任务)

    4.调度函数OSSched()

    非常简单,刚才我们将某个任务变为了非就绪态,紧接着就找就绪态任务中优先级最高的任务,然后切换

    5.是否完成了呢?----空闲任务

    乍一看,好像完成了,实则不然,虽然我们任务1每X毫秒运行一次,任务2每Y毫秒运行一次,但终归里面有空闲时间:任务1和任务2都不在运行,所以我们需要创建一个空闲任务,当CPU没有东西可以运行时,运行空闲任务。以下就是空闲任务:

    6.来到主函数:

    其他的倒是没问题,72行有个陌生的函数:OS_TASK_Init();

    其实就是之前的OSStartHighRdy()函数的升级版,非常简单

    先创建一个空闲函数,再获取优先级最高的任务,然后执行最高优先级的任务。

    7.一个优雅的简易OS诞生了

    不好看?想加个界面?没问题-----其实已经有了,大家观察58行,就是让液晶屏显示一句话,我们把背景修改为红色,醒目一点:



    很开心能有这么多朋友喜欢,非常感谢 。我开了一个简单的头,相信真的喜欢os的朋友,只要认真去做,一定也能实现一个更好的作品。后面的暂时不打算写,如果有了新的思路,一定会再写出来。授人以鱼不如授人以渔,看到这里已经有动手的能力了,想写的朋友不要害怕,尽管去做!!!加油


    五、加入信号量
    六、加入消息队列
    七、内存管理
    八、实现一个free和malloc玩玩?

    编辑于 2016-12-05

    ​赞同 492​​50 条评论

    ​分享

    ​收藏​感谢收起​

    logo中国婚博会

    广告​

    不感兴趣知乎广告介绍

    中国婚博会(上海站)限量门票免费领取中!

    2019年8月24-25日中国婚博会(上海站)世博展览馆开展!荟萃婚庆、摄影、珠宝、婚品、婚车...2天帮您搞定婚礼,门票免费快递到家!查看详情

    fleuria

    fleuria

    刀剑的时代已经过去,接下来的战斗要拿起枪

    246 人赞同了该回答

    也可以参考我的毕业论文: 基于 Bochs 的操作系统内核实现

    汇编不重要,但是要有一定计算机组成的基础,并对一个现代 kernel 的结构有大体的认识,至少大致上理解虚拟内存和文件系统有哪些东西。不要看 《the orange's》和《三十天编操作系统》,面太小,代码质量不高,就别拿 DOS 当操作系统了。个人比较推荐《莱昂氏 UNIX 源码分析》(已绝版,可淘宝打印)、《linux 0.11 内核详解/剖析》 ,写代码之前至少先都啃一遍。教程的话推荐 《bran's kernel development tutorial》和 osdev 上的一些资料。顺着它们搭开发环境,出一个简单的 bootlaoder,可以编译 C 代码即可。然后拿大把大把的时间慢慢给 bootloader 加东西就好了,能用 C 就不要用汇编。开发中的很多细节要到开发时才留意的到,这时可以自己思考,也可以去抄 linux 0.11, xv6, unixv6 这几套优秀的源码。

    现在想来,开发一个 kernel 的主要内容在于“实现”而不是“设计”,更重要的是用时间去理解这些优秀的设计为什么合理,自己别出心裁的想法一般不用多想,一定是错的。

    不要在 x86 的一些历史遗留问题上花太多时间,比如 bootloader 的保护模式会在开头挡住一大批人,可是这并不重要,只要知道有这个接口可以引导你的二进制代码即可。知道 GDT 可以区分用户态/内核态,IDT 可以给中断绑回调就行了。在调试上会花费大量时间,可以慢慢琢磨怎样提高调试的效率。然后需要的就只是耐性了,说实话也挺无聊。

    编辑于 2018-03-02

    ​赞同 246​​30 条评论

    ​分享

    ​收藏​感谢

    周仕成

    周仕成

    uiuc cs专业

    33 人赞同了该回答

    我来骗一下star哈哈
    szhou42/osdev
    虚拟内存✔
    硬盘驱动,EXT2文件系统,虚拟文件系统✔
    简陋的GUI界面✔
    多进程(烂尾楼 :) )✔
    网络数据收发(只是简单收发raw packets,各种网络协议还在写)✔




    这是我学习过程中用到的主要网站和资料

    BrokenThorn Entertainment

    Global Descriptor Table

    Unofficial Mirror for JamesM's Kernel Development Tutorial Series

    Expanded Main Page


    Build software better, together :)

    楼上大神全是几千字长文看起来有点怕,我简明扼要地说一些写OS的重要心得吧

    1 教程上没看懂的代码千万不要照抄,最好在自己理解的基础上重新写一遍。我在看JamesM's Kernel教程的paging部分时就吃了这个亏,全盘抄了他的代码,结果发现他的代码总有神奇的bugs让你的os崩溃。结果我把写了一个半月的代码全部推翻,在完全理解后自己重新写出来的paging代码,不能说完全没bugs吧,至少现在已经四五个月了都没有再因为paging部分的代码而使os崩溃。

    2 搭建方便使用的调试器,我个人用的是qemu+gdb配合,源码级调试

    3 在os最基础的设施(中断,异常,VGA driver)都实现后,马上写个printf和hexdump函数,因为有一些极端情况,gdb下断点+单步跟踪+观察变量 这种办法会失效 :(。

    4 快点写个malloc函数!越快越好!!文件系统,进程管理,GUI这些都需要用到大量的数据结构,而最方便的方法就是用malloc来申请和释放这些结构。

    最后,说一下os开发的流程,这只是我个人的路线。
    第一步,很多人会想写bootloader,但是我建议先跳过这一步,直接用grub或者qemu的自带bootloader,先跳过这些繁琐的细节,专注于OS内核的开发。

    第二步,建立好各种gdt,idt,中断,异常等机制,这样系统出什么错的时候马上就能发现。

    第三步,printf函数,这意味着你得先写VGA driver,但两者都不难

    第四步,实现虚拟内存和分页机制,在此基础上实现kmalloc函数。

    第五步,实现多进程/线程

    第六步,写个PCI驱动!PCI是用来访问各种硬件的,例如硬盘,网卡,都得通过PCI来控制,实际上我就是因为想写硬盘驱动,才写的PCI驱动。

    第七步,写硬盘驱动,实现EXT2文件系统,实现VFS文件系统。

    第八步,GUI,设计一种数据结构存储和显示各种窗口。初步可以用VGA试验一下编写图形操作系统的乐趣,但是要想有高分辨率 真彩色还是得写VESA驱动才能得到。 很多人觉得图形操作系统很酷炫,但这反而是写OS里面最简单,最容易调试的一步(当然了要做VESA驱动还是很麻烦,因为在保护模式下没法用中断调用)。

    第九步,实现网卡驱动,实现TCP/IP协议栈!

    第十步,发挥你的想象!Network File System maybe?

    编辑于 2017-01-05

    ​赞同 33​​2 条评论

    ​分享

    ​收藏​感谢收起​

    frank yao

    frank yao

    虎妈猫爸

    19 人赞同了该回答

    我大约是在08-09年的时候写过一个迷你的操作系统。大家可以在这里看到我的源代码 https://code.google.com/p/minios2/。当时这个项目完全是自己的兴趣爱好。后来代码较多了觉得需要花费过多的精力不合适就放弃了。

     

    整个项目是从0开始的。因为主要在windows上开发,所以主力编译器是msvc6.0。虽然说很不可思议。但是当你明白了编译链接的原理以及PE文件的格式之后,这其实并不难。当然现在如果用高版本的msvc写的话会更容易。

     

    另一个难点是需要寻找以及阅读大量的资料。包括比如386保护模式,bios调用,8259中断控制器,pci总线控制器,8253时钟控制器,ATA硬盘控制器,各种以太网卡控制器等。当时这些资料在网上非常分散,收集很不容易。现在貌似好找多了。

     

    另外你需要对数据结构,计算机体系结构以及操作系统原理有一定的了解。这个基本上本科和研究生课程里的知识就足够了。当然你也要有足够的编程经验,因为有些错误可能会很难调试。


     

    以下是我当时写的一个简单的文档。

    minios目前已经完成的功能:

    bootsector

    进入保护模式

    内存分配模块

    简单的线程调度模块

    信号量

    时钟

    延时调用DPC和定时调用TPC

    统一的设备驱动模型

    标准输入输出设备驱动

    内存及字符串相关的标准C库函数

    附件codes.zip的目录结构如下:

    codes

    |-relocate 连接程序的源代码,将bootsector和minios连接成一个可启动的磁盘镜像

    |-bootsector bootsector的源代码

    |-minios minios的源代码

    |-bin 所有的目标都在此目录中。其中minios.vhd就是可启动的磁盘镜像

    如何启动minios:

    你必须安装Microsoft的Virtual PC 2007

    你可以在微软的官方网站下载他的安装程序,程序大小约30M

    http://download.microsoft.com/download/8/5/6/856bfc39-fa48-4315-a2b3-e6697a54ca88/32%20BIT/setup.exe

    安装完成后就可以双击codes/bin/vm.vmc运行minios了

    如何编译minios:

    编译minios共需要三种编译器。

    codes/bootsector/bootsector.asm必须用nasm进行编译,将编译的结果命名为bootsector并且拷贝到codes/bin

    codes/minios/platform/platform.asm必须用masm32进行编译,编译的结果在codes/minios/platform/platform.obj

    其余的代码都用vc6编译即可,vc6的工程在codes/minios/minios.dsw

    如果你手边没有nasm和masm32,不要紧,因为这两个文件一般不需要改动,直接用我编译好的目标文件就可以了

    双击minios.dsw打开vc6,点击菜单Project->Project Setting->Debug,修改Executable for debug session一栏

    将Virtual PC.exe的完整路径填入。如果你安装在默认的路径下,就不需要修改它。

    然后直接Ctrl-F5运行就可以编译并且运行了。

     

    vc工程是在dll的工程的基础上配置的

    1、将所有相关的文件加到工程中来。

    2、由于对于debug版本的代码生成,vc会加入不少调试代码,不好控制,所以删除Win32 Debug的配置

    3、由于默认的Release配置中,会加入Intrinsic Functions的优化,他会用vc libc中的函数代替你写的标准C语言库函数。因此必须自定义优化方案。project setting->C++->Optimizations选customize并且勾上除了Assume No Aliasing, Generate Intrinsic Functions, Favor Small Code, Full Optimization外的优化选项。

    4、在project setting->C++->Preprocessor->Additional include directories中加入include这个目录,并且勾上Ignore standard include paths

    5、project setting->Link中,output file name改成../bin/minios.dll。勾上Ignore all default libraries和Generate mapfile, object/libraty modules中的内容清空

    6、project setting->Link->Debug中mapfile name改成../bin/minios.map,project setting->Link->Output中Entry-point symbol改成main

    7、project setting->post-build step中添加一行"../bin/relocate.exe" ../bin/minios.dll ../bin/bootsector ../bin/minios.vhd

    8、project setting->Debug中Executable for debug session改成C:\Program Files\Microsoft Virtual PC\Virtual PC.exe,working directory改成../bin,Program arguments改成-singlepc -pc vm -launch

    如果我没有忘记什么的话,应该就这些了。这样你的vc就可以编译minios的原代码了。编译的结果在../bin/minios.dll

    为什么使用dll的工程呢?

    因为windows的dll中有一个relocation的段,他列出了该dll文件如果要重定位的话所有需要修改的地址偏移。假设dll默认的加载位置是0x10000000,而在minios中我希望把它定位在0x400000则只需要把重定位表的每一项所指向的地址减去(0x10000000- 0x400000)就可以了。这也是relocate.exe这个程序的主要工作。

    至于具体pe文件的结构以及重定向表的结构,网上有很多,我手边暂时没有资料,可以参看relocate.exe的原代码

    minios的引导过程和内存布局

    首先,pc机的bios程序会将bootsector加载到0x7C00, 此时段寄存器的值我也不大清楚,但是不要紧,自己重新设一遍吧。把ds, es, ss都设成cs一样的值,把sp放在0x8000的位置上,这样我们就有了512字节的堆栈了。

    然后,bootsector将minios读出放在从0x400000开始的内存空间上。随后bootsector简单的设置了GDT后直接进入保护模式并且将控制权交给minios的entrypoint。从0x100000到0x3fffff是内核堆,内核所需要的动态内存都可以从此堆上使用keMalloc和keFree分配。内存最低的4K字节被用来存放中断向量指针,就像纯DOS那样。从0x4000开始到0x8000存放了PCI总线配置数据块。从0x10000到0x1ffff的64K字节用来作为IDE的DMA内存块,其他的低端内存暂时还没有分配,可能会作为文件系统的缓存。

    main函数先重新配置了8259中断控制器,8253时钟控制器,设置了IDT GDT,初始化时钟,内核堆和任务子系统后,建立了第一个任务main,入口是keEntryMain

    keEntryMain首先打开中断并且加载console和keboard的驱动,然后建立DPC任务,kdebug任务以及一个测试任务

    编辑于 2015-11-11

    ​赞同 19​​1 条评论

    ​分享

    ​收藏​感谢收起​

    Pandaos

    Pandaos

    不会数学的信安菜狗,立志转行生物信息学,三本肄业

    15 人赞同了该回答

    (年代久远凭记忆写,有错误请指出)

    已经过去五年了,现在想起来仍然觉得热血沸腾,致那个中二的年纪~~ ~我已经很久没碰代码了,现在很迷茫,偶然看到这个话题,才回想起来我曾经也是一个有梦想的少年。

     

    我曾经学习8086汇编的时候,有幸认识了一个开发OS的朋友,他为我打开了一个新世界的大门。他建立了一个QQ群里来交流OS开发心得。。开发OS的有两类人,一类是喜欢技术喜欢折腾的,另外一类是喜欢拉帮结派搞计划的。我很反感第二类,个人写的OS顶多就是用来学习底层知识的,上不了台面,有些人非觉得自己很牛逼要搞点大事情出来。我是一个比较保守的人,很长一段时间对这些写OS的人嗤之以鼻,我觉得他们写的就是一个裸机程序,有的代码量还没我用易语言写的QQ机器人的代码多,竟然自称OS?

     

    我在开发OS之前翻了翻《Windows 内核情景分析》。当时还在学习Windows内核驱动,这本书可以学习ReactOS(仿WinNT内核)各方面的实现,比如系统调用,内存分配等等。书分为上下两册,很厚,其实我看得云里雾里的。还看过Linux内核源码剖析方面的书籍。我那个时候看过很多书,但是都很晕。可贵的是我有心思去看,去思考。现在再让我看这些书籍,我心里不太愿意,长大了,开始变得功利起来。

     

    入门一般从引导程序开始,引导程序存储在mbr,加电后,cpu默认处于实模式。实模式下的汇编主要可以参考王爽的汇编教材。一般mbr引导程序只有几百个字节大小,所以需要完成进一步加载,加载一个更大的引导程序。 初始化过程中需要把CPU切到保护模式,保护模式的书籍可以参考李忠的《href="http://www.baidu.com/link?url=hRMBwBQb7-jpSQFzdNVo1HCEb4szWKlw6-duNjU1WF_yb2qs0noVLcGOlnMebUoWkZgCKOlbAsOLZwbOWUQ46_">x86汇编语言 从实模式到保护模式完整版》 。如果还想专业一点可以参考《x86/x64体系探索及编程 (邓志)》或者intel的官方手册。《30天自制操作系统》我个人不太喜欢,感觉没有oranges专业。

     

    引导程序都是用汇编写的,即使一个再简单的内核也需要很多代码,全用汇编不太现实。引导程序需要想办法加载用高级语言编写的程序。现在的编译器目标二进制比较常见的是elf和pe,这就需要进一步学习elf或pe文件结构知识。建议选elf,装载器有源码参考。解析文件格式可能很繁琐,也可以直接将elf或pe中的bin脱出来用或者自己设计文件格式,但是要考虑重定位等问题。

     

    我折腾OS的时候,基础数理知识极度欠缺,比如画图只会画直线,于是放弃了UI开发路线,转向命令行模式。内存分配,各种乱七八糟的算法都搞不懂,于是自己意淫了一种方法出来,碎片?不存在的,反正目前阶段内存是用不完的。。。后来,学pwn的时候认真阅读了glibc的malloc实现,才感觉到曾经的方法是多么弱智。

    我写OS之前写过一段时间win驱动,对windbg极度依赖,寻思着能不能用易语言搞一个类似的调试器,折腾一段时间后我放弃了。串口通信搞定了,搞不定如何单步,各种莫名奇妙的错误。根本原因还是OS的很多基本工作没有做完就想着做调试体系,而且没有任何一本书讲过这个。后来干脆模仿windows蓝屏,把错误信息打在屏幕上。

    文件系统。我对实用性有着很高的追求,自己实现简易文件系统虽然来得快(不健全有文件系统的样子),但是不实用。于是选择先折腾fat32,这又是一个巨坑。后来,我发现网传的硬盘读写方法巨坑,只有ide能用,这是我放弃开发的原因之一。

     

    总之,写os不要觉得自己在搞大事,我们都是学习者。

    编辑于 2019-05-04

    ​赞同 15​​2 条评论

    ​分享

    ​收藏​感谢收起​

    林瑟

    林瑟

    嘟嘟嘟嘟嘟嘟嘟

    17 人赞同了该回答

    照片删掉了,大家专心看技术吧

    终于!!!一个混大数据圈的我也能有朝一日来回答这个话题了。请在座的各位多多包涵,下面我就要开始我的表演了!!!

    先把我的原文地址安排在这里,请大家走过路过不要错过,随手点击支持一下我这个可爱的女程序员吧(你支持我我支持我导师:):

    操作系统基础: C 语言实现用户态线程(实战)​gitbook.cn图标

     

    ——————一本正经的美颜分割线————————

    之前我上大学老师讲的时候大概说过这个操作系统的知识,那会只知道是最底层的语言,了解原理活学活用就行,之后开始工作了也没用得上,就渐渐都忘干净了,直到有一天重新抹着泪捡起来,这个过程也挺有意思的呢。

    我从零开始捡起的时候,主要是学习了线程切换和角度,看了大量的操作系统的书,还有 Linux 的源码甚至反汇编了 Windows 的内核代码吧啦吧啦的,反正最后没看懂!!(简直闻着伤心听者落泪呜呜呜)

    爆哭

    书和源码都太抽象了,学起来超级困难的呀,但是因为第一份工作,要懂那些用户态线程的基本知识,但我这方面的基本功还不扎实,那段日子我都瘦了(想想还是很难过...)后来我听公司的前辈们说 Golang 的 Goroutine,鹅场开源的 libco,狼场 BRPC 中的 bhtread 都离不开这个,我也只好硬着头皮去搞了。

     

    好的各位哥哥前辈们,我的硬核知识来了,先安排一下我的实验环境(突然正经:)

    • ubuntu 16.04 32 位操作系统(最好是提前安装好哦);
    • 挑选一个你觉得好用的虚拟机软件,比如 VMWare;
    • 把虚拟机环境配置成单核 CPU。

    呵 这么复杂的内容,想不到吧,我自己还搞了四五个小时呢!!

    但是你肯定要问了,这都 9012 年了为啥还在用 32 位的系统尼??!

    毕竟是初学者,请各位大佬谅解谅解哈,我也是为了快速掌握原理嘛

    最后的效果图就是这样滴~~

    oh 对了 我用 C 语言搞出来了,果然我大学老师说得没错, C 简直是万能的啊,只有想不到,没有做不到!!

    想给大家提醒的是,上面的代码,并没有使用操作系统为我们提供的pthread系列函数,thread_createthread_join函数都是自己纯手工实现的。唯一使用操作系统的函数就是设置时钟,因此会有时钟信号产生,这一步是为了模拟时间片轮转算法而做的。

    贴一段我上面动图的 demo 示例代码:

    这篇文章预计是个长文,所以可能不会一下子更新完毕。。。。。

    所以先把我要说的一些点列出:

    • 控制流切换原理
    • 上下文切换
    • 线程设计
    • 调度函数的封装与代码模块化
    • 线程的主动切换
    • 时间片轮转调度

    小姐妹叫我吃饭了哈哈哈哈哈哈回来再更吧哈哈哈哈哈哈

    恰饭!!

    ——————第二天的分割线——————

    哦果然,没人注意到我这个小透明,那我可以放心叭叭叭了。这篇教程来自于我在公司的导师,简直拯救我于水火中的大佬,所以来炫耀一下成果哈哈哈哈哈

    早起更新,先来说一下控制流切换原理

    控制流,指的是一系列按顺序执行的指令。多控制流,是指存在两个或两个以上可以并发(宏观同时,微观不同时)执行的指令序列。比如你编写的多线程程序,每个线程就可以看成是一个控制流,多个线程允许多个控制流一起执行。

    在我们学习编程的时候,如果不借助操作系统提供的线程框架,几乎无法完成多控制流的运行的。

    接下来先来剖析一下,我们的指令如何”莫名奇妙“的就切换到其它线程的。

    1.1 指令执行

    不管你用的是什么语言编程,最后都要落实到 CPU 上,而 CPU 只认识它自己的语言,机器语言。机器语言可以抽象出对应 CPU 架构的汇编指令。如下面的 x86 指令序列。

    程序在执行时,实际上就是汇编指令(准确的说是机器指令)在 CPU 上一条一条执行。对于单核 CPU 来说,永远只有一条控制流,也就是只有一条指令序列。所以,宏观上模拟的多线程程序,本质上还只是单控制流,所谓的多线程,只不过是一种被制造出来的假像!

    注:有部分同学没有接触过汇编指令,不要害怕,我们用到的汇编不会太难!

    1.2 控制流切换(x86 CPU 架构)

    汇编指令在执行的时候,最重要地方在于它需要依赖 CPU 环境:

    • 一套通用寄存器 (eax、edx、ecx、ebx、esp、ebp、esi、edi);
    • 标志寄存器 eflags;
    • 指令寄存器 eip (eip 用来保存下一条要被指令的地址)。

    如果你不理解 CPU 寄存器是什么意思,把它想象成它是 CPU 中预先定义好的变量。也不知道大家有没有看懂唉,我觉得我导师这里讲的还是蛮清楚的啊啊啊啊啊啊真的佩服了!!大佬就是我学习的榜样啊喂!

    操作系统基础: C 语言实现用户态线程(实战)​gitbook.cn图标

    如果各位有更好的方法在评论区给我留言讨论一下哦,不然我就默默更贴了。。。。

    还有一个很重要环境,就是。因为指令序列在执行时,经常会保存一些临时数据,比如某条指令的地址。当指令执行 ret 指令的时候,CPU 会从当前栈顶弹出一个值到 eip 寄存器!这意味着要发生跳转了!

    通用寄存器中,有一个寄存器名为 esp,它保存的是栈顶指针(内存地址的值)。指令 push 、 pop、call、ret 都依赖于 esp 工作。

    • call 指令把它后面的指令地址保存到 esp 指向的内存单元,同时修改 eip。如 call 0x2000,先把 call 0x2000 的下一条指令地址压栈,同时修改 eip 为 0x2000。
    • ret 指令把 esp 指向的内存单元中的值保存到 eip。
    • push 指令把值保存到 esp 指向的内存单元。
    • pop 把 esp 指向的内存单元的值取出。

     

    图2 CPU 寄存器 esp 与内存单元的关系,右侧表示运行栈

    想象一下,如果某个时候,我们把 esp 保存的数据 “偷偷” 换了,换句话说我们是把栈换了,而栈中保存的那个“某条指令”的地址的值也不一样了,将会发生什么?图3 把 esp 的值从 0xFFE8 更改成了 0x1FFE8。

     

    图3 切换 esp 即切换栈

    所谓的切换控制流,无非就是更改 esp 栈顶指针来做到偷梁换柱的把戏而已。只不过,为了做到惟妙惟肖,我们在更改 esp 的时候,还得顺带的把通用寄存器环境修改修改,以适应新的那段“某条指令”的执行环境。(注意,栈中经常会保存某条指令的地址,比如函数的返回地址。)

    通常,这段新的“某条指令”的执行环境,恰好也保存在栈里,就像上图中 esp 到“某条指令地址”之间那段内存的数据(五颜六色的那部分数据)。

    说了这么多,其实也很抽象对不对,大家可以去看我导师举的栗子,还是很一目了然的,我就不在这里贴出来了,为什么呢。因为他写的文章太长太长又太详细,需要去专心的读,再加上动手操作试试,我当初看到这篇指导教程的时候作为徒弟内心有一万个不服,我就不信自己一个外行的看完了就能搞明白???????

    然而事实证明,我打脸了,我导师的这篇文,真的很强,不会是狼场的码农,真的够狠!!!剩下的文章,大家点上面的原文链接去看吧,我可能有点佛系不会按时更新(说白了就是忙。。。)

    好嘞,我们下次更新时再见吧!!请走过路过的小哥哥小姐姐们给我点个赞吧,谢谢哦~

    编辑于 2019-05-08

    ​赞同 17​​8 条评论

    ​分享

    ​收藏​感谢收起​

    我现在只想搞钱

    我现在只想搞钱

    23 人赞同了该回答

    这个问题码了得有一两年了。。终于算是有资格来回答这个问题。


    先上代码

    项目地址:MRNIU/SimpleKernel

    这是我正在写的内核,目的是尽量让刚刚接触的人可以基于已有代码拓展出自己的内核来。

    • 基本信息

    语言:C/C++, shell, x86 ATT Assembly

    启动:GRUB2

    Kernel Model:宏内核

    虚拟机:bochs

    支持平台:Linux/Mac

    • 进度

    已完成:

    1. GDT、IDT 的基本设置
    2. 键盘输入
    3. libc 中的部分函数
    4. 中断体系的框架

    TODO:

    1. 内存管理
    2. debug
    3. 进程
    4. 图形界面
    5. POSIX API
    6. 设备管理
    7. 网络
    8. etc.

    目前正在将启动支持迁移到 multiboot2,顺便把内存管理和 debug 的部分前置内容做了。

    这是目录结构

    ├── bochsout.txt bochs 输出
    ├── bochsrc.txt bochs 配置文件
    ├── setup.sh
    ├── simplekernel.img
    ├── someknowledge
    ├── src/ 源码目录
    │   ├── Makefile 构建规则
    │   ├── arch/ 架构相关代码
    │   │   ├── README.md
    │   │   ├── i386/ i386 架构
    │   │   │   ├── README.md
    │   │   │   ├── boot/ 启动代码,使用 multiboot
    │   │   │   │   ├── boot.s
    │   │   │   │   └── link.ld
    │   │   │   ├── clock.c 时钟
    │   │   │   ├── clock.h
    │   │   │   ├── cpu.hpp CPU操作
    │   │   │   ├── debug/ 调试函数
    │   │   │   │   └── debug.c
    │   │   │   ├── intr/ 中断设置
    │   │   │   │   ├── README.md
    │   │   │   │   ├── intr.c idt 设置
    │   │   │   │   ├── intr.h
    │   │   │   │   └── intr_s.s
    │   │   │   └── mm/ 内存管理
    │   │   │       ├── README.md
    │   │   │       ├── gdt.c gdt 设置
    │   │   │       ├── gdt.h
    │   │   │       ├── gdt_s.s
    │   │   │       ├── pmm.c 物理内存管理
    │   │   │       ├── pmm.h
    │   │   │       ├── vmm.c 虚拟内存管理
    │   │   │       └── vmm.h
    │   │   └── x64/ x64 架构
    │   │       └── TODO
    │   ├── include/ 头文件
    │   │   ├── DataStructure/ 可能会用到的数据结构与算法
    │   │   │   ├── BinarySearchTree.cpp 二叉树
    │   │   │   ├── DataStructuer.h
    │   │   │   ├── LinkedList.cpp 链表
    │   │   │   ├── Queue.cpp 队列
    │   │   │   ├── SortAlgorithm.cpp 排序算法
    │   │   │   └── Stack.cpp 栈
    │   │   ├── README.md
    │   │   ├── debug.h
    │   │   ├── drv/ 设备头文件
    │   │   │   ├── keyboard.h
    │   │   │   └── mouse.h
    │   │   ├── elf.h elf 格式定义
    │   │   ├── kernel.h 内核函数直接引用的头文件
    │   │   ├── libc/ c 标准库
    │   │   │   ├── README.md
    │   │   │   ├── assert.h 断言
    │   │   │   ├── stdarg.h
    │   │   │   ├── stdbool.h
    │   │   │   ├── stddef.h
    │   │   │   ├── stdint.h
    │   │   │   ├── stdio/ 标准输入输出
    │   │   │   │   ├── printk.c
    │   │   │   │   └── vsprintf.c
    │   │   │   ├── stdio.h
    │   │   │   ├── string/ 字符串处理
    │   │   │   │   └── string.c
    │   │   │   ├── string.h
    │   │   ├── mm/ 内存相关头文件
    │   │   │   ├── README.md
    │   │   │   └── mm.h
    │   │   ├── multiboot.h 多重引导规范定义
    │   │   ├── pic.hpp 8259A 中断芯片设置
    │   │   ├── port.hpp 端口操作
    │   │   ├── tty.hpp tty 定义
    │   │   └── vga.hpp vga 显示定义
    │   └── kernel/
    │       ├── README.md
    │       ├── drv/ 设备
    │       │   ├── kb.c
    │       │   └── mouse.c
    │       └── kernel.c 内核入口
    └── tools/ 工具,在 .rb 文件中你可以找到 gcc 和 binutils 的编译选项
        ├── i386-elf-binutils.rb
        └── i386-elf-gcc.rb
    
    
    

    欢迎讨论

    发布于 2019-01-07

    ​赞同 23​​2 条评论

    ​分享

    ​收藏​感谢收起​

    Coldwings

    Coldwings

    Python 话题的优秀回答者

    6 人赞同了该回答

    操作系统这玩意…并不是都像windows那样图形界面一堆工具,甚至不像linux发行版那样带一堆命令行工具。
    以linux为例,图形界面就不说了命令行?那是bash,是个独立软件包,人家在bsd在unix在darwin上都跑得妥妥的。
    一个纯粹的操作系统,其实只是定义了驱动接口(用别人的驱动),定义了最简单的进程调度管理,定义了内存分配。这就已经是操作系统了。
    所以写一个新的操作系统真的真的不是特别困难。困难的是你的os出来之后除了你自己大概是不会有人给他写驱动写程序的,除非用户多;啥都没有的os不会有人用。于是恶性循环…

    发布于 2015-02-08

    ​赞同 6​​1 条评论

    ​分享

    ​收藏​感谢

    lizhimeng159

    lizhimeng159

    知易行难/INTJ

    13 人赞同了该回答

    寒假开始有个想法,突然想写一个最简单的操作系统,基于单片机的。

    于是找人借来学校索奥社团的板子,历时四天,写了个atmega16的os内核,能实现,任务调度,任务内嵌信号量,任务延迟,具体效果如下

     

    大概就是这样的
    任务建立如下如图

    三个任务

    流水灯,蜂鸣器,数码管。

    之后再更具体制作过程和所需要求。

    (突然发现不能放视频....哪一张图片那闪几个灯的自动忽略吧,本来想发一下跑任务的视频的)

    一,关于所需要的(储备)知识

    1,微机原理

    emmmm,用到的有关微机原理的东西就是 汇编

    能用到的指令大概就是 POP 和 PUSH这种级别的..

    然后需要知道 堆栈保存现场的原理

    2,单片机原理及其应用

    主要是,这个内核是基于单片机的,还是基于AVR单片机,所以就不说哪些高大上的linux什么东西。

    中断和定时器得知道怎么回事吧。

    主要是知道SP(堆栈指针),PC(程序计数器)是怎么回事,并且在什么时候(主动或者被动)修改他们

    3,C语言

    这里面用到的C语言也就有下面几个东西

    指针(如函数指针等),struct,typedef ,与或运算,for循环,while等等,

    有点编程基础都会。

    4,单片机RTOS

    这个,我觉得会不会无所谓吧,说到底这个内核是超简单的内核,没用什么算法,遍历什么的都是for循环....简而言之,就是,简单易懂原理。

    二,原理概述

    不如我打个比方。

    假设人A在一个屋子里,屋子里有三个房间,分别是卧室,客厅,厨房,主人要在一上午的时间里完成如下事情。

    事情一:

    卧室很乱,需要人打扫整理。

    事情二:

    电话在客厅里,有可能别人来打电话谈事情。

    事情三:

    厨房正在烧水,到水烧开后,主任A要去厨房断点拿水壶。

    论重要性,则有水烧开后拿水壶>接电话>收拾卧室。

    场景:

    1,A在收拾卧室,此时电话响了,A停止收拾,卧室现场被保存(卧室不变)

    2,A去客厅接电话讨论事情,在接电话过程中,水烧开了,水壶给出信号,A给对方说,先停止这个讨论,讨论内容此时被记录(下次结着这次内容继续讨论)

    3,A去厨房处理水壶,处理完成







     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • Java面试Offer直通车

    万人学习 2019-12-18 15:19:52
    500道大厂Java常见面试题+100个Java面试技巧与答题公式+10字核心知识解析+授课老师1对1面试指导+无限次回放 2、这门课程基于胡书敏老师8年Java面试经验,调研近百家互联网公司及面试官的问题打造而成,筛选简历...
  • MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS (Relational Database Management System:关系数据库管理系统) 应用软件之
    • MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一

    在这里插入图片描述

    MySQL实战文章目录


    MySQL必会知识点梳理 (必看)

    在这里插入图片描述

    评论区评论要资料三个字即可获得MySQL全套资料 !


    【介绍】

    在这里插入图片描述

    什么是数据库

    • 数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。
    • 每个数据库都有一个或多个不同的 API 用于创建,访问,管理,搜索和复制所保存的数据。
    • 我们也可以将数据存储在文件中,但是在文件中读写数据速度相对较慢。所以,现在我们使用关系型数据库管理系统(RDBMS)来存储和管理大数据量。所谓的关系型数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。

    MySQL数据库

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。

    • MySQL 是开源的,目前隶属于 Oracle 旗下产品。
    • MySQL 支持大型的数据库。可以处理拥有上千万条记录的大型数据库。
    • MySQL 使用标准的 SQL 数据语言形式。
    • MySQL 可以运行于多个系统上,并且支持多种语言。这些编程语言包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl 等。
    • MySQL 对PHP有很好的支持,PHP 是目前最流行的 Web 开发语言。
    • MySQL 支持大型数据库,支持 5000 万条记录的数据仓库,32 位系统表文件最大可支持 4GB,64 位系统支持最大的表文件为8TB。
    • MySQL 是可以定制的,采用了 GPL 协议,你可以修改源码来开发自己的 MySQL 系统。

    RDBMS 术语

    在我们开始学习MySQL 数据库前,让我们先了解下RDBMS的一些术语

    • 数据库: 数据库是一些关联表的集合。
    • 数据表: 表是数据的矩阵。在一个数据库中的表看起来像一个简单的电子表格。
    • 列: 一列(数据元素) 包含了相同类型的数据, 例如邮政编码的数据。
    • 行:一行(=元组,或记录)是一组相关的数据,例如一条用户订阅的数据。
    • 冗余:存储两倍数据,冗余降低了性能,但提高了数据的安全性。
    • 主键:主键是唯一的。一个数据表中只能包含一个主键。你可以使用主键来查询数据。
    • 外键:外键用于关联两个表。
    • 复合键:复合键(组合键)将多个列作为一个索引键,一般用于复合索引。
    • 索引:使用索引可快速访问数据库表中的特定信息。索引是对数据库表中一列或多列的值进行排序的一种结构。类似于书籍的目录。
    • 参照完整性: 参照的完整性要求关系中不允许引用不存在的实体。与实体完整性是关系模型必须满足的完整性约束条件,目的是保证数据的一致性。

    MySQL 为关系型数据库(Relational Database Management System), 这种所谓的关系型可以理解为表格的概念, 一个关系型数据库由一个或数个表格组成, 如图所示的一个表格

    数据库表的存储位置

    MySQL数据表以文件方式存放在磁盘中:

    1. 包括表文件、数据文件以及数据库的选项文件
    2. 位置:MySQL安装目录\data下存放数据表。目录名对应数据库名,该目录下文件名对应数据表

    注:

    InnoDB类型数据表只有一个*. frm文件,以及上一级目录的ibdata1文件
    MylSAM类型数据表对应三个文件:

    1. *. frm —— 表结构定义文件
    2. *. MYD —— 数据文件
    3. *. MYI —— 索引文件

    存储位置:因操作系统而异,可查my.ini


    【数据类型】

    • MySQL提供的数据类型包括数值类型(整数类型和小数类型)、字符串类型、日期类型、复合类型(复合类型包括enum类型和set类型)以及二进制类型 。

    一. 整数类型

    在这里插入图片描述

    • 整数类型的数,默认情况下既可以表示正整数又可以表示负整数(此时称为有符号数)。如果只希望表示零和正整数,可以使用无符号关键字“unsigned”对整数类型进行修饰。
    • 各个类别存储空间及取值范围。

    在这里插入图片描述

    二. 小数类型

    在这里插入图片描述

    • decimal(length, precision)用于表示精度确定(小数点后数字的位数确定)的小数类型,length决定了该小数的最大位数,precision用于设置精度(小数点后数字的位数)。

    • 例如: decimal (5,2)表示小数取值范围:999.99~999.99 decimal (5,0)表示: -99999~99999的整数。

    • 各个类别存储空间及取值范围。

    在这里插入图片描述

    三. 字符串

    在这里插入图片描述

    • char()与varchar(): 例如对于简体中文字符集gbk的字符串而言,varchar(255)表示可以存储255个汉字,而每个汉字占用两个字节的存储空间。假如这个字符串没有那么多汉字,例如仅仅包含一个‘中’字,那么varchar(255)仅仅占用1个字符(两个字节)的储存空间;而char(255)则必须占用255个字符长度的存储空间,哪怕里面只存储一个汉字。
    • 各个类别存储空间及取值范围。

    在这里插入图片描述

    四. 日期类型

    • date表示日期,默认格式为‘YYYY-MM-DD’; time表示时间,格式为‘HH:ii:ss’; year表示年份; datetime与timestamp是日期和时间的混合类型,格式为’YYYY-MM-DD HH:ii:ss’。
      在这里插入图片描述
    • datetime与timestamp都是日期和时间的混合类型,区别在于: 表示的取值范围不同,datetime的取值范围远远大于timestamp的取值范围。 将NULL插入timestamp字段后,该字段的值实际上是MySQL服务器当前的日期和时间。 同一个timestamp类型的日期或时间,不同的时区,显示结果不同。
    • 各个类别存储空间及取值范围。

    在这里插入图片描述

    五. 复合类型

    • MySQL 支持两种复合数据类型:enum枚举类型和set集合类型。 enum类型的字段类似于单选按钮的功能,一个enum类型的数据最多可以包含65535个元素。 set 类型的字段类似于复选框的功能,一个set类型的数据最多可以包含64个元素。

    六. 二进制类型

    • 二进制类型的字段主要用于存储由‘0’和‘1’组成的字符串,因此从某种意义上将,二进制类型的数据是一种特殊格式的字符串。
    • 二进制类型与字符串类型的区别在于:字符串类型的数据按字符为单位进行存储,因此存在多种字符集、多种字符序;而二进制类型的数据按字节为单位进行存储,仅存在二进制字符集binary。

    在这里插入图片描述


    【约束】

    • 约束是一种限制,它通过对表的行或列的数据做出限制,来确保表的数据的完整性、唯一性。下面文章就来给大家介绍一下6种mysql常见的约束,希望对大家有所帮助。

    一. 非空约束(not null)

    • 非空约束用于确保当前列的值不为空值,非空约束只能出现在表对象的列上。

    • Null类型特征:所有的类型的值都可以是null,包括int、float 等数据类型

    在这里插入图片描述

    二. 唯一性约束(unique)

    • 唯一约束是指定table的列或列组合不能重复,保证数据的唯一性。
    • 唯一约束不允许出现重复的值,但是可以为多个null。
    • 同一个表可以有多个唯一约束,多个列组合的约束。
    • 在创建唯一约束时,如果不给唯一约束名称,就默认和列名相同。
    • 唯一约束不仅可以在一个表内创建,而且可以同时多表创建组合唯一约束。

    在这里插入图片描述

    三. 主键约束(primary key) PK

    • 主键约束相当于 唯一约束 + 非空约束 的组合,主键约束列不允许重复,也不允许出现空值。

    • 每个表最多只允许一个主键,建立主键约束可以在列级别创建,也可以在表级别创建。

    • 当创建主键的约束时,系统默认会在所在的列和列组合上建立对应的唯一索引。

    在这里插入图片描述

    四. 外键约束(foreign key) FK

    • 外键约束是用来加强两个表(主表和从表)的一列或多列数据之间的连接的,可以保证一个或两个表之间的参照完整性,外键是构建于一个表的两个字段或是两个表的两个字段之间的参照关系。

    • 创建外键约束的顺序是先定义主表的主键,然后定义从表的外键。也就是说只有主表的主键才能被从表用来作为外键使用,被约束的从表中的列可以不是主键,主表限制了从表更新和插入的操作。
      在这里插入图片描述

    五. 默认值约束 (Default)

    • 若在表中定义了默认值约束,用户在插入新的数据行时,如果该行没有指定数据,那么系统将默认值赋给该列,如果我们不设置默认值,系统默认为NULL。

    在这里插入图片描述

    六. 自增约束(AUTO_INCREMENT)

    • 自增约束(AUTO_INCREMENT)可以约束任何一个字段,该字段不一定是PRIMARY KEY字段,也就是说自增的字段并不等于主键字段。

    • 但是PRIMARY_KEY约束的主键字段,一定是自增字段,即PRIMARY_KEY 要与AUTO_INCREMENT一起作用于同一个字段。

    在这里插入图片描述

    当插入第一条记录时,自增字段没有给定一个具体值,可以写成DEFAULT/NULL,那么以后插入字段的时候,该自增字段就是从1开始,没插入一条记录,该自增字段的值增加1。当插入第一条记录时,给自增字段一个具体值,那么以后插入的记录在此自增字段上的值,就在第一条记录该自增字段的值的基础上每次增加1。也可以在插入记录的时候,不指定自增字段,而是指定其余字段进行插入记录的操作。


    【常用命令】

    登录数据库相关命令

    一. 启动服务

    语法:

    mysql> net stop mysql
    

    二. 关闭服务

    语法:

    mysql> net start mysql
    

    三. 链接MySQL

    • 语法:mysql -u用户名 -p密码;
    root@243ecf24bd0a:/ mysql -uroot -p123456;
    
    • 在以上命令行中,mysql 代表客户端命令,-u 后面跟连接的数据库用户,-p 表示需要输入密码。如果数据库设置正常,并输入正确的密码,将看到上面一段欢迎界面和一个 mysql>提示符。
      在这里插入图片描述

    四. 退出数据库

    • 语法:quit
    mysql> quit
    
    • 结果:
      在这里插入图片描述

    DDL(Data Definition Languages)语句:即数据库定义语句

    对于数据库而言实际上每一张表都表示是一个数据库的对象,而数据库对象指的就是DDL定义的所有操作,例如:表,视图,索引,序列,约束等等,都属于对象的操作,所以表的建立就是对象的建立,而对象的操作主要分为以下三类语法

    • 创建对象:CREATE 对象名称;
    • 删除对象:DROP 对象名称;
    • 修改对象:ALTER 对象名称;

    数据库相关操作

    在这里插入图片描述

    一. 创建数据库

    • 语法:create database 数据库名字;
    mysql> create database sqltest;
    
    • 结果:
      在这里插入图片描述

    二. 查看已经存在的数据库

    • 语法:show databases;
    mysql> show databases;
    
    • 结果:
      在这里插入图片描述

    可以发现,在上面的列表中除了刚刚创建的 mzc-test,sqltest,外,还有另外 4 个数据库,它们都是安装MySQL 时系统自动创建的,其各自功能如下。

    1. information_schema:主要存储了系统中的一些数据库对象信息。比如用户表信息、列信息、权限信息、字符集信息、分区信息等。
    2. cluster:存储了系统的集群信息。
    3. mysql:存储了系统的用户权限信息。
    4. test:系统自动创建的测试数据库,任何用户都可以使用。

    三. 选择数据库

    • 语法:use 数据库名;
    mysql> use mzc-test;
    
    • 返回Database changed代表我们已经选择 sqltest 数据库,后续所有操作将在 sqltest 数据库上执行。
      在这里插入图片描述
    • 有些人可能会问到,连接以后怎么退出。其实,不用退出来,use 数据库后,使用show databases就能查询所有数据库,如果想跳到其他数据库,用use 其他数据库名字。

    四. 查看数据库中的表

    • 语法:show tables;
    mysql> show tables;
    
    • 结果:
      在这里插入图片描述

    五. 删除数据库

    • 语法:drop database 数据库名称;
    mysql> drop database mzc-test;
    
    • 结果:
      在这里插入图片描述
    • 注意:删除时,最好用 `` 符号把表明括起来

    六. 设置表的类型

    • MySQL的数据表类型:MyISAMInnoDB、HEAP、 BOB、CSV等

    在这里插入图片描述
    语法:

    CREATE TABLE 表名(
    	#省略代码ENGINE= InnoDB;
    

    适用场景:

    1. 使用MyISAM:节约空间及响应速度快;不需事务,空间小,以查询访问为主
    2. 使用InnoDB:安全性,事务处理及多用户操作数据表;多删除、更新操作,安全性高,事务处理及并发控制
    
    1. 查看mysql所支持的引擎类型

    语法:

    SHOW ENGINES
    

    结果:

    在这里插入图片描述

    2. 查看默认引擎

    语法:

    SHOW VARIABLES LIKE 'storage_engine';
    

    结果:
    在这里插入图片描述


    数据库表相关操作

    在这里插入图片描述

    一. 创建表

    语法:create table 表名 {列名,数据类型,约束条件};

    CREATE TABLE `Student`(
    	`s_id` VARCHAR(20),
    	`s_name` VARCHAR(20) NOT NULL DEFAULT '',
    	`s_birth` VARCHAR(20) NOT NULL DEFAULT '',
    	`s_sex` VARCHAR(10) NOT NULL DEFAULT '',
    	PRIMARY KEY(`s_id`)
    );
    
    • 结果
      在这里插入图片描述

    注意:表名还请遵守数据库的命名规则,这条数据后面要进行删除,所以首字母为大写。

    二. 查看表定义

    • 语法:desc 表名
    mysql> desc Student;
    
    • 结果:
      在这里插入图片描述
    • 虽然 desc 命令可以查看表定义,但是其输出的信息还是不够全面,为了查看更全面的表定义信息,有时就需要通过查看创建表的 SQL 语句来得到,可以使用如下命令实现
    • 语法:show create table 表名 \G;
    mysql> show create table Student \G;
    
    • 结果:
      在这里插入图片描述
    • 从上面表的创建 SQL 语句中,除了可以看到表定义以外,还可以看到表的engine(存储引擎)和charset(字符集)等信息。\G选项的含义是使得记录能够按照字段竖着排列,对于内容比较长的记录更易于显示。

    三. 删除表

    • 语法:drop table 表名
    mysql> drop table Student;
    
    • 结果:

    在这里插入图片描述

    四. 修改表 (重要)

    • 对于已经创建好的表,尤其是已经有大量数据的表,如果需要对表做一些结构上的改变,我们可以先将表删除(drop),然后再按照新的表定义重建表。这样做没有问题,但是必然要做一些额外的工作,比如数据的重新加载。而且,如果有服务在访问表,也会对服务产生影响。因此,在大多数情况下,表结构的更改一般都使用 alter table语句,以下是一些常用的命令。
    1. 修改表类型
    • 语法:ALTER TABLE 表名 MODIFY [COLUMN] column_definition [FIRST | AFTER col_name]
    • 例如,修改表 student 的 s_name 字段定义,将 varchar(20)改为 varchar(30)
    mysql> alter table Student modify s_name varchar(30);
    
    • 结果:
      在这里插入图片描述
    2. 增加表字段
    • 语法:ALTER TABLE 表名 ADD [COLUMN] [FIRST | AFTER col_name];
    • 例如,表 student 上新增加字段 s_test,类型为 int(3)
    mysql> alter table student add column s_test int(3);
    
    • 结果:
      在这里插入图片描述
    3. 删除表字段
    • 语法:ALTER TABLE 表名 DROP [COLUMN] col_name
    • 例如,将字段 s_test 删除掉
    mysql> alter table Student drop column s_test;
    
    • 结果:
      在这里插入图片描述
    4. 字段改名
    • 语法:ALTER TABLE 表名 CHANGE [COLUMN] old_col_name column_definition [FIRST|AFTER col_name]
    • 例如,将 s_sex 改名为 s_sex1,同时修改字段类型为 int(4)
    mysql> alter table Student change s_sex s_sex1 int(4);
    
    • 结果:
      在这里插入图片描述

    注意:change 和 modify 都可以修改表的定义,不同的是 change 后面需要写两次列名,不方便。但是 change 的优点是可以修改列名称,modify 则不能。

    5. 修改字段排列顺序
    • 前面介绍的的字段增加和修改语法(ADD/CNAHGE/MODIFY)中,都有一个可选项first|after column_name,这个选项可以用来修改字段在表中的位置,默认 ADD 增加的新字段是加在表的最后位置,而 CHANGE/MODIFY 默认都不会改变字段的位置。

    • 例如,将新增的字段 s_test 加在 s_id 之后

    • 语法:alter table 表名 add 列名 数据类型 after 列名;

    mysql> alter table Student add s_test date after s_id;
    
    • 结果:
      在这里插入图片描述
    • 修改已有字段 s_name,将它放在最前面
    mysql> alter table Student modify s_name varchar(30) default '' first;
    
    • 结果:
      在这里插入图片描述

    注意:CHANGE/FIRST|AFTER COLUMN 这些关键字都属于 MySQL 在标准 SQL 上的扩展,在其他数据库上不一定适用。

    6.表名修改
    • 语法:ALTER TABLE 表名 RENAME [TO] new_tablename
    • 例如,将表 Student 改名为 student
    mysql> alter table Student rename student;
    
    • 结果:
      在这里插入图片描述

    DML(Data Manipulation Language)语句:即数据操纵语句

    • 用于操作数据库对象中所包含的数据

    一. 添加数据:INSERT

    Insert 语句用于向数据库中插入数据

    1. 插入单条数据(常用)

    语法:insert into 表名(列名1,列名2,...) values(值1,值2,...)

    特点:

    • 插入值的类型要与列的类型一致或兼容。插入NULL可实现为列插入NULL值。列的顺序可以调换。列数和值的个数必须一致。可省略列名,默认所有列,并且列的顺序和表中列的顺序一致。

    案例:

    -- 插入学生表测试数据
    insert into Student(s_id,s_name,s_birth,s_sex) values('01' , '赵信' , '1990-01-01' , '男');
    

    在这里插入图片描述

    2. 插入单条数据

    语法:INSERT INTO 表名 SET 列名 = 值,列名 = 值

    • 这种方式每次只能插入一行数据,每列的值通过赋值列表制定。

    案例:

    INSERT INTO student SET s_id='02',s_name='德莱厄斯',s_birth='1990-01-01',s_sex='男'
    

    在这里插入图片描述

    3. 插入多条数据

    语法:insert into 表名 values(值1,值2,值3),(值4,值5,值6),(值7,值8,值9);

    案例:

    INSERT INTO student VALUES('03','艾希','1990-01-01','女'),('04','德莱文','1990-08-06','男'),('05','俄洛依','1991-12-01','女');
    

    在这里插入图片描述

    上面的例子中,值1,值2,值3),(值4,值5,值6),(值7,值8,值9) 即为 Value List,其中每个括号内部的数据表示一行数据,这个例子中插入了三行数据。Insert 语句也可以只给部分列插入数据,这种情况下,需要在 Value List 之前加上 ColumnName List,

    例如:

    INSERT INTO student(s_name,s_sex) VALUES('艾希','女'),('德莱文','男');
    
    • 每行数据只指定了 s_name 和 s_sex 这两列的值,其他列的值会设为 Null。

    4. 表数据复制

    语法:INSERT INTO 表名 SELECT * from 表名;

    案例:

    INSERT INTO student SELECT * from student1;
    

    在这里插入图片描述注意:

    • 两个表的字段需要一直,并尽量保证要新增的表中没有数据

    二. 更新数据:UPDATE

    Update 语句一共有两种语法,分别用于更新单表数据和多表数据。

    在这里插入图片描述

    • 注意:没有 WHERE 条件的 UPDATE 会更新所有值!

    1. 修改一条数据的某个字段

    语法:UPDATE 表名 SET 字段名 =值 where 字段名=值

    案例:

    UPDATE student SET s_name ='张三' WHERE s_id ='01'
    

    在这里插入图片描述

    2. 修改多个字段为同一的值

    语法:UPDATE 表名 SET 字段名= 值 WHERE 字段名 in ('值1','值2','值3');

    案例:

    UPDATE student SET s_name = '李四' WHERE s_id in ('01','02','03');
    

    在这里插入图片描述

    3. 使用case when实现批量更新

    语法:update 表名 set 字段名 = case 字段名 when 值1 then '值' when 值2 then '值' when 值3 then '值' end where s_id in (值1,值2,值3)

    案例:

    update student set s_name = case s_id when 01 then '小王' when 02 then '小周' when 03 then '老周' end where s_id in (01,02,03)
    

    在这里插入图片描述

    • 这句sql的意思是,更新 s_name 字段,如果 s_id 的值为 01 则 s_name 的值为 小王,s_id = 02 则 s_name = 小周,如果s_id =03 则 s_name 的值为 老周。
    • 这里的where部分不影响代码的执行,但是会提高sql执行的效率。确保sql语句仅执行需要修改的行数,这里只有3条数据进行更新,而where子句确保只有3行数据执行。

    案例 2:

    UPDATE student SET s_birth = CASE s_name
    	WHEN '小王' THEN
    		'2019-01-20'
    	WHEN '小周' THEN
    		'2019-01-22'
    END WHERE s_name IN ('小王','小周');
    

    在这里插入图片描述

    三. 删除数据:DELETE

    • 数据库一旦删除数据,它就会永远消失。 因此,在执行DELETE语句之前,应该先备份数据库,以防万一要找回删除过的数据。

    1. 删除指定数据

    语法:DELETE FROM 表名 WHERE 列名=值

    • 注意:删除的时候如果不指定where条件,则保留数据表结构,删除全部数据行,有主外键关系的都删不了

    案例:

    DELETE FROM student WHERE s_id='09'
    

    在这里插入图片描述与 SELECT 语句不同的是,DELETE 语句中不能使用 GROUP BY、 HAVING 和 ORDER BY 三类子句,而只能使用WHERE 子句。原因很简单, GROUP BY 和 HAVING 是从表中选取数据时用来改变抽取数据形式的, 而 ORDER BY 是用来指定取得结果显示顺序的。因此,在删除表中数据 时它们都起不到什么作用。`

    2. 删除表中全部数据

    语法:TRUNCATE 表名;

    • 注意:全部删除,内存无痕迹,如果有自增会重新开始编号。

    • 与 DELETE 不同的是,TRUNCATE 只能删除表中的全部数据,而不能通过 WHERE 子句指定条件来删除部分数据。也正是因为它不能具体地控制删除对象, 所以其处理速度比 DELETE 要快得多。实际上,DELETE 语句在 DML 语句中也 属于处理时间比较长的,因此需要删除全部数据行时,使用 TRUNCATE 可以缩短 执行时间。

    案例:

    TRUNCATE student1;
    

    在这里插入图片描述


    DQL(Data Query Language)语句:即数据查询语句

    • 查询数据库中的记录,关键字 SELECT,这块内容非常重要!

    一. wherer 条件语句

    语法:select 列名 from 表名 where 列名 =值

    where的作用:

    1. 用于检索数据表中符合条件的记录
    2. 搜索条件可由一个或多个逻辑表达式组成,结果一般为真或假

    搜索条件的组成:

    • 算数运算符

    在这里插入图片描述

    • 逻辑操作符(操作符有两种写法)
      在这里插入图片描述
    • 比较运算符

    在这里插入图片描述

    注意:数值数据类型的记录之间才能进行算术运算,相同数据类型的数据之间才能进行比较。

    表数据
    在这里插入图片描述

    案例 1(AND):

    SELECT  * FROM student WHERE s_name ='小王' AND s_sex='男'
    

    在这里插入图片描述
    案例 2(OR):

    SELECT  * FROM student WHERE s_name ='崔丝塔娜' OR s_sex='男'
    

    在这里插入图片描述

    案例 3(NOT):

    SELECT  * FROM student WHERE NOT s_name ='崔丝塔娜' 
    

    在这里插入图片描述
    案例 4(IS NULL):

    SELECT * FROM student WHERE s_name IS NULL;
    

    在这里插入图片描述
    案例 5(IS NOT NULL):

    SELECT * FROM student WHERE s_name IS NOT NULL;
    

    在这里插入图片描述
    案例 6(BETWEEN):

    SELECT * FROM student WHERE s_birth BETWEEN '2019-01-20' AND '2019-01-22'
    

    在这里插入图片描述

    案例 7(LINK):

    SELECT * FROM student WHERE s_name LIKE '小%'
    

    在这里插入图片描述

    案例 8(IN):

    SELECT * FROM student WHERE s_name IN ('小王','小周')
    

    在这里插入图片描述

    二. as 取别名

    • 表里的名字没有变,只影响了查询出来的结果

    案例:

    SELECT s_name as `name` FROM student 
    

    在这里插入图片描述

    • 使用as也可以为表取别名 (作用:单表查询意义不大,但是当多个表的时候取别名就好操作,当不同的表里有相同名字的列的时候区分就会好区分)

    三. distinct 去除重复记录

    • 注意:当查询结果中所有字段全都相同时 才算重复的记录

    案例

    SELECT DISTINCT * FROM student
    

    在这里插入图片描述

    指定字段

    1. 星号表示所有字段
    2. 手动指定需要查询的字段
    SELECT DISTINCT s_name,s_birth FROM student
    

    在这里插入图片描述

    1. 还可也是四则运算
    2. 聚合函数

    四. group by 分组

    • group by的意思是根据by对数据按照哪个字段进行分组,或者是哪几个字段进行分组。

    语法:

    select 字段名 from 表名 group by 字段名称;
    

    1. 单个字段分组

    SELECT COUNT(*)FROM student GROUP BY s_sex;
    

    在这里插入图片描述

    2. 多个字段分组

    SELECT s_name,s_sex,COUNT(*) FROM student GROUP BY s_name,s_sex;
    

    在这里插入图片描述

    • 注意:多个字段进行分组时,需要将s_name和s_sex看成一个整体,只要是s_name和s_sex相同的可以分成一组;如果只是s_sex相同,s_sex不同就不是一组。

    五. having 过滤

    • HAVING 子句对 GROUP BY 子句设置条件的方式与 WHERE 和 SELECT 的交互方式类似。WHERE 搜索条件在进行分组操作之前应用;而 HAVING 搜索条件在进行分组操作之后应用。HAVING 语法与 WHERE 语法类似,但 HAVING 可以包含聚合函数。HAVING 子句可以引用选择列表中显示的任意项。

    我们如果要查询男生或者女生,人数大于4的性别

    
    SELECT s_sex as 性别,count(s_id) AS 人数 FROM student GROUP BY s_sex HAVING COUNT(s_id)>4
    

    在这里插入图片描述

    六. order by 排序

    • 根据某个字段排序,默认升序(从小到大)

    语法:

    select * from 表名 order by 字段名;
    

    1. 一个字段,降序(从大到小)

    SELECT * FROM student ORDER BY s_id DESC;
    

    在这里插入图片描述

    2. 多个字段

    SELECT * FROM student ORDER BY s_id DESC, s_birth ASC;
    

    在这里插入图片描述

    • 多个字段 第一个相同在按照第二个 asc 表示升序

    limit 分页

    • 用于限制要显示的记录数量

    语法1:

    select * from table_name limit 个数;
    

    语法2:

    select * from table_name limit 起始位置,个数;
    

    案例:

    • 查询前三条数据
    SELECT * FROM student LIMIT 3;
    

    在这里插入图片描述

    • 从第三条开始 查询3条
    SELECT * FROM student LIMIT 2,3;
    

    在这里插入图片描述

    注意:起始位置 从0开始

    经典的使用场景:分页显示

    1. 每一页显示的条数 a = 3
    2. 明确当前页数 b = 2
    3. 计算起始位置 c = (b-1) * a

    子查询

    • 将一个查询语句的结果作为另一个查询语句的条件或是数据来源,​ 当我们一次性查不到想要数据时就需要使用子查询。
    SELECT
    	* 
    FROM
    	score 
    WHERE
    	s_id =(
    	SELECT
    		s_id 
    	FROM
    		student 
    WHERE
    	s_name = '赵信')
    

    在这里插入图片描述

    1. in 关键字子查询

    • 当内层查询 (括号内的) 结果会有多个结果时, 不能使用 = 必须是in ,另外子查询必须只能包含一列数据

    子查询的思路:

    1. 要分析 查到最终的数据 到底有哪些步骤
    2. 根据步骤写出对应的sql语句
    3. 把上一个步骤的sql语句丢到下一个sql语句中作为条件
    SELECT
    	* 
    FROM
    	score 
    WHERE
    	s_id IN (
    	SELECT
    		s_id 
    	FROM
    		student 
    WHERE
    	s_sex = '男')
    

    在这里插入图片描述

    exists 关键字子查询

    • 当内层查询 有结果时 外层才会执行

    多表查询

    1. 笛卡尔积查询

    • 笛卡尔积查询的结果会出现大量的错误数据即,数据关联关系错误,并且会产生重复的字段信息 !

    2. 内连接查询

    • 本质上就是笛卡尔积查询,inner可以省略。

    在这里插入图片描述

    语法:

    select * from1 inner join2;
    

    3. 左外连接查询

    • 左边的表无论是否能够匹配都要完整显示,右边的仅展示匹配上的记录

    在这里插入图片描述

    • 注意: 在外连接查询中不能使用where 关键字 必须使用on专门来做表的对应关系

    4. 右外连接查询

    • 右边的表无论是否能够匹配都要完整显示,左边的仅展示匹配上的记录

    在这里插入图片描述


    DCL(Data Control Language)语句:即数据控制语句

    • DCL(Data Control Language)语句:数据控制语句,用于控制不同数据段直接的许可和访问级别的语句。这些语句定义了数据库、表、字段、用户的访问权限和安全级别。

    关键字

    • GRANT
    • REVOKE

    查看用户权限

    当成功创建用户账户后,还不能执行任何操作,需要为该用户分配适当的访问权限。可以使用SHOW GRANTS FOR语句来查询用户的权限。

    例如:

    mysql> SHOW GRANTS FOR test;
    +-------------------------------------------+
    | Grants for test@%                         |
    +-------------------------------------------+
    | GRANT ALL PRIVILEGES ON *.* TO 'test'@'%' |
    +-------------------------------------------+
    1 row in set (0.00 sec)
    

    GRANT语句

    • 对于新建的MySQL用户,必须给它授权,可以用GRANT语句来实现对新建用户的授权。

    格式语法

    GRANT
        priv_type [(column_list)]
          [, priv_type [(column_list)]] ...
        ON [object_type] priv_level
        TO user [auth_option] [, user [auth_option]] ...
        [REQUIRE {NONE | tls_option [[AND] tls_option] ...}]
        [WITH {GRANT OPTION | resource_option} ...]
    
    GRANT PROXY ON user
        TO user [, user] ...
        [WITH GRANT OPTION]
    
    object_type: {
        TABLE
      | FUNCTION
      | PROCEDURE
    }
    
    priv_level: {
        *
      | *.*
      | db_name.*
      | db_name.tbl_name
      | tbl_name
      | db_name.routine_name
    }
    
    user:
        (see Section 6.2.4, “Specifying Account Names”)
    
    auth_option: {
        IDENTIFIED BY 'auth_string'
      | IDENTIFIED WITH auth_plugin
      | IDENTIFIED WITH auth_plugin BY 'auth_string'
      | IDENTIFIED WITH auth_plugin AS 'auth_string'
      | IDENTIFIED BY PASSWORD 'auth_string'
    }
    
    tls_option: {
        SSL
      | X509
      | CIPHER 'cipher'
      | ISSUER 'issuer'
      | SUBJECT 'subject'
    }
    
    resource_option: {
      | MAX_QUERIES_PER_HOUR count
      | MAX_UPDATES_PER_HOUR count
      | MAX_CONNECTIONS_PER_HOUR count
      | MAX_USER_CONNECTIONS count
    }
    

    权限类型(priv_type)

    • 授权的权限类型一般可以分为数据库、表、列、用户。
    授予数据库权限类型

    授予数据库权限时,priv_type可以指定为以下值:

    • SELECT:表示授予用户可以使用 SELECT 语句访问特定数据库中所有表和视图的权限。
    • INSERT:表示授予用户可以使用 INSERT 语句向特定数据库中所有表添加数据行的权限。
    • DELETE:表示授予用户可以使用 DELETE 语句删除特定数据库中所有表的数据行的权限。
    • UPDATE:表示授予用户可以使用 UPDATE 语句更新特定数据库中所有数据表的值的权限。
    • REFERENCES:表示授予用户可以创建指向特定的数据库中的表外键的权限。
    • CREATE:表示授权用户可以使用 CREATE TABLE 语句在特定数据库中创建新表的权限。
    • ALTER:表示授予用户可以使用 ALTER TABLE 语句修改特定数据库中所有数据表的权限。
    • SHOW VIEW:表示授予用户可以查看特定数据库中已有视图的视图定义的权限。
    • CREATE ROUTINE:表示授予用户可以为特定的数据库创建存储过程和存储函数的权限。
    • ALTER ROUTINE:表示授予用户可以更新和删除数据库中已有的存储过程和存储函数的权限。
    • INDEX:表示授予用户可以在特定数据库中的所有数据表上定义和删除索引的权限。
    • DROP:表示授予用户可以删除特定数据库中所有表和视图的权限。
    • CREATE TEMPORARY TABLES:表示授予用户可以在特定数据库中创建临时表的权限。
    • CREATE VIEW:表示授予用户可以在特定数据库中创建新的视图的权限。
    • EXECUTE ROUTINE:表示授予用户可以调用特定数据库的存储过程和存储函数的权限。
    • LOCK TABLES:表示授予用户可以锁定特定数据库的已有数据表的权限。
    • SHOW DATABASES:表示授权可以使用SHOW DATABASES语句查看所有已有的数据库的定义的权限。
    • ALL或ALL PRIVILEGES:表示以上所有权限。

    授予表权限类型

    授予表权限时,priv_type可以指定为以下值:

    • SELECT:授予用户可以使用 SELECT 语句进行访问特定表的权限。
    • INSERT:授予用户可以使用 INSERT 语句向一个特定表中添加数据行的权限。
    • DELETE:授予用户可以使用 DELETE 语句从一个特定表中删除数据行的权限。
    • DROP:授予用户可以删除数据表的权限。
    • UPDATE:授予用户可以使用 UPDATE 语句更新特定数据表的权限。
    • ALTER:授予用户可以使用 ALTER TABLE 语句修改数据表的权限。
    • REFERENCES:授予用户可以创建一个外键来参照特定数据表的权限。
    • CREATE:授予用户可以使用特定的名字创建一个数据表的权限。
    • INDEX:授予用户可以在表上定义索引的权限。
    • ALL或ALL PRIVILEGES:所有的权限名。
    授予列(字段)权限类型
    • 授予列(字段)权限时,priv_type的值只能指定为SELECT、INSERT和UPDATE,同时权限的后面需要加上列名列表(column-list)。
    授予创建和删除用户的权限
    • 授予列(字段)权限时,priv_type的值指定为CREATE USER权限,具备创建用户、删除用户、重命名用户和撤消所有特权,而且是全局的。

    ON

    • 有ON,是授予权限,无ON,是授予角色。如:
    -- 授予数据库db1的所有权限给指定账户
    GRANT ALL ON db1.* TO 'user1'@'localhost';
    -- 授予角色给指定的账户
    GRANT 'role1', 'role2' TO 'user1'@'localhost', 'user2'@'localhost';
    

    对象类型(object_type)

    • 在ON关键字后给出要授予权限的object_type,通常object_type可以是数据库名、表名等。

    权限级别(priv_level)

    指定权限级别的值有以下几类格式:

    • *:表示当前数据库中的所有表。
    • .:表示所有数据库中的所有表。
    • db_name.*:表示某个数据库中的所有表,db_name指定数据库名。
    • db_name.tbl_name:表示某个数据库中的某个表或视图,db_name指定数据库名,tbl_name指定表名或视图名。
    • tbl_name:表示某个表或视图,tbl_name指定表名或视图名。
    • db_name.routine_name:表示某个数据库中的某个存储过程或函数,routine_name指定存储过程名或函数名。

    被授权的用户(user)

    'user_name'@'host_name'
    
    • Tips:'host_name’用于适应从任意主机访问数据库而设置的,可以指定某个地址或地址段访问。
    • 可以同时授权多个用户。

    user表中host列的默认值

    host说明
    %匹配所有主机
    localhostlocalhost不会被解析成IP地址,直接通过UNIXsocket连接
    127.0.0.1会通过TCP/IP协议连接,并且只能在本机访问
    ::1::1就是兼容支持ipv6的,表示同ipv4的127.0.0.1

    host_name格式有以下几种:

    • 使用%模糊匹配,符合匹配条件的主机可以访问该数据库实例,例如192.168.2.%或%.test.com;
    • 使用localhost、127.0.0.1、::1及服务器名等,只能在本机访问;
    • 使用ip地址或地址段形式,仅允许该ip或ip地址段的主机访问该数据库实例,例如192.168.2.1或192.168.2.0/24或192.168.2.0/255.255.255.0;
    • 省略即默认为%。

    身份验证方式(auth_option)

    • auth_option为可选字段,可以指定密码以及认证插件(mysql_native_password、sha256_password、caching_sha2_password)。

    加密连接(tls_option)

    • tls_option为可选的,一般是用来加密连接。

    用户资源限制(resource_option)

    • resource_option为可选的,一般是用来指定最大连接数等。
    参数说明
    MAX_QUERIES_PER_HOUR count每小时最大查询数
    MAX_UPDATES_PER_HOUR count每小时最大更新数
    MAX_CONNECTIONS_PER_HOUR count每小时连接次数
    MAX_USER_CONNECTIONS count用户最大连接数

    权限生效

    • 若要权限生效,需要执行以下语句:
    FLUSH PRIVILEGES;
    

    REVOKE语句

    • REVOKE语句主要用于撤销权限。

    语法格式

    • REVOKE语法和GRANT语句的语法格式相似,但具有相反的效果
    REVOKE
        priv_type [(column_list)]
          [, priv_type [(column_list)]] ...
        ON [object_type] priv_level
        FROM user [, user] ...
    
    REVOKE ALL [PRIVILEGES], GRANT OPTION
        FROM user [, user] ...
    
    REVOKE PROXY ON user
        FROM user [, user] ...
    
    • 若要使用REVOKE语句,必须拥有MySQL数据库的全局CREATE USER权限或UPDATE权限;
    • 第一种语法格式用于回收指定用户的某些特定的权限,第二种回收指定用户的所有权限;

    TCL(Transaction Control Language)语句:事务控制语句

    什么是事物?

    • 一个或一组sql语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行

    事务的ACID属性

    • 原子性:事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生

    • 一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态

    • 隔离性:一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰

    • 持久性:一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响

    分类

    • 隐式事务:事务没有明显的开启和结束的标记(比如insert,update,delete语句)

    • 显式事务:事务具有明显的开启和结束的标记(autocommit变量设置为0)

    事务的使用步骤

    开启事务

    • 默认开启事务
    SET autocommit = 0 ;
    

    提交事务

    COMMIT;
    

    回滚事务

    ROLLBACK ;
    

    查看当前的事务隔离级别

    select @@tx_isolation;
    

    设置当前连接事务的隔离级别

    set session transaction isolation level read uncommitted;
    

    设置数据库系统的全局的隔离级别

    set global transaction isolation level read committed ;
    

    【常用函数】

    • MySQL提供了众多功能强大、方便易用的函数,使用这些函数,可以极大地提高用户对于数据库的管理效率,从而更加灵活地满足不同用户的需求。本文将MySQL的函数分类并汇总,以便以后用到的时候可以随时查看。

    (这里使用 Navicat Premium 15 工具进行演示)

    在这里插入图片描述

    因为内容太多了这里只演示一些常用的在这里插入图片描述

    一. 数学函数

    对数值型的数据进行指定的数学运算,如abs()函数可以获得给定数值的绝对值,round()函数可以对给定的数值进行四舍五入。

    1. ABS(number)

    • 作用:返回 number 的绝对值
    SELECT
     ABS(s_score)
    FROM
    	score;
    

    在这里插入图片描述

    在这里插入图片描述

    • ABS(-86) 返回:86

    • number 参数可以是任意有效的数值表达式。如果 number 包含 Null,则返回 Null;如果是未初始化变量,则返回 0。

    2. PI()

    • 例1:pi() 返回:3.141592653589793

    • 例2:pi(2) 返回:6.283185307179586

    • 作用:计算圆周率及其倍数

    3. SQRT(x)

    • 作用:返回非负数的x的二次方根

    4. MOD(x,y)

    • 作用:返回x被y除后的余数

    5. CEIL(x)、CEILING(x)

    • 作用:返回不小于x的最小整数

    6. FLOOR(x)

    • 作用:返回不大于x的最大整数

    7. FLOOR(x)

    • 作用:返回不大于x的最大整数

    8. ROUND(x)、ROUND(x,y)

    • 作用:前者返回最接近于x的整数,即对x进行四舍五入;后者返回最接近x的数,其值保留到小数点后面y位,若y为负值,则将保留到x到小数点左边y位
    SELECT ROUND(345222.9)
    

    在这里插入图片描述

    • 参数说明: numberExp 需要进行截取的数据 nExp 整数,用于指定需要进行截取的位置,>0:从小数点往右位移nExp个位数, <0:从小数点往左

    nExp个位数 =0:表示当前小数点的位置

    9. POW(x,y)和、POWER(x,y)

    • 作用:返回x的y次乘方的值

    10. EXP(x)

    • 作用:返回e的x乘方后的值

    11. LOG(x)

    • 作用:返回x的自然对数,x相对于基数e的对数

    12. LOG10(x)

    • 作用:返回x的基数为10的对数

    13. RADIANS(x)

    • 作用:返回x由角度转化为弧度的值

    14. DEGREES(x)

    • 作用:返回x由弧度转化为角度的值

    15. SIN(x)、ASIN(x)

    • 作用:前者返回x的正弦,其中x为给定的弧度值;后者返回x的反正弦值,x为正弦

    16. COS(x)、ACOS(x)

    • 作用:前者返回x的余弦,其中x为给定的弧度值;后者返回x的反余弦值,x为余弦

    17. TAN(x)、ATAN(x)

    • 作用:前者返回x的正切,其中x为给定的弧度值;后者返回x的反正切值,x为正切

    18. COT(x)

    • 作用:返回给定弧度值x的余切

    二. 字符串函数

    1. CHAR_LENGTH(str)

    • 作用:计算字符串字符个数
    SELECT CHAR_LENGTH('这是一个十二个字的字符串');
    

    在这里插入图片描述

    2. CONCAT(s1,s2,…)

    • 作用:返回连接参数产生的字符串,一个或多个待拼接的内容,任意一个为NULL则返回值为NULL
    SELECT CONCAT('拼接','测试');
    

    在这里插入图片描述

    3. CONCAT_WS(x,s1,s2,…)

    • 作用:返回多个字符串拼接之后的字符串,每个字符串之间有一个x
    SELECT CONCAT_WS('-','测试','拼接','WS') 
    

    在这里插入图片描述

    4. INSERT(s1,x,len,s2)

    • 作用:返回字符串s1,其子字符串起始于位置x,被字符串s2取代len个字符
    SELECT INSERT('测试字符串替换',2,1,'牛');
    

    在这里插入图片描述

    5. LOWER(str)和LCASE(str)、UPPER(str)和UCASE(str)

    • 作用:前两者将str中的字母全部转换成小写,后两者将字符串中的字母全部转换成大写
    SELECT LOWER('JHGYTUGHJGG'),LCASE('HKJHKJHKJHKJ');
    

    在这里插入图片描述

    SELECT UPPER('aaaaaa'),UCASE('vvvvv');
    

    在这里插入图片描述

    6. LEFT(s,n)、RIGHT(s,n)

    • 作用:前者返回字符串s从最左边开始的n个字符,后者返回字符串s从最右边开始的n个字符
    SELECT LEFT('左边开始',2),RIGHT('右边开始',2);
    

    在这里插入图片描述

    7. LPAD(s1,len,s2)、RPAD(s1,len,s2)

    • 作用:前者返回s1,其左边由字符串s2填补到len字符长度,假如s1的长度大于len,则返回值被缩短至len字符;前者返回s1,其右边由字符串s2填补到len字符长度,假如s1的长度大于len,则返回值被缩短至len字符
    SELECT LEFT('左边开始',2),RIGHT('右边开始',2);
    

    在这里插入图片描述

    8. LTRIM(s)、RTRIM(s)

    • 作用:前者返回字符串s,其左边所有空格被删除;后者返回字符串s,其右边所有空格被删除
    SELECT LTRIM('       左边开始'),RTRIM('    右边开始         ');
    

    在这里插入图片描述

    9. TRIM(s)

    • 作用:返回字符串s删除了两边空格之后的字符串
    SELECT TRIM(' 是是 ');
    

    在这里插入图片描述

    10. TRIM(s1 FROM s)

    • 作用:删除字符串s两端所有子字符串s1,未指定s1的情况下则默认删除空格

    11. REPEAT(s,n)

    • 作用:返回一个由重复字符串s组成的字符串,字符串s的数目等于n
    SELECT REPEAT('测试',5);
    

    在这里插入图片描述

    12. SPACE(n)

    • 作用:返回一个由n个空格组成的字符串
    SELECT SPACE(20);
    

    在这里插入图片描述

    13. REPLACE(s,s1,s2)

    • 作用:返回一个字符串,用字符串s2替代字符串s中所有的字符串s1

    14. STRCMP(s1,s2)

    • 作用:若s1和s2中所有的字符串都相同,则返回0;根据当前分类次序,第一个参数小于第二个则返回-1,其他情况返回1
    SELECT STRCMP('我我我','我我我');
    

    在这里插入图片描述

    SELECT STRCMP('我我我','是是是');
    

    在这里插入图片描述

    15. SUBSTRING(s,n,len)、MID(s,n,len)

    • 作用:两个函数作用相同,从字符串s中返回一个第n个字符开始、长度为len的字符串
    SELECT SUBSTRING('测试测试',2,2);
    

    在这里插入图片描述

    SELECT MID('测试测试',2,2);
    

    在这里插入图片描述

    16. LOCATE(str1,str)、POSITION(str1 IN str)、INSTR(str,str1)

    • 作用:三个函数作用相同,返回子字符串str1在字符串str中的开始位置(从第几个字符开始)
    SELECT LOCATE('字','获取字符串的位置');
    

    在这里插入图片描述

    17. REVERSE(s)

    • 作用:将字符串s反转
    SELECT REVERSE('字符串反转');
    

    在这里插入图片描述

    18. ELT(N,str1,str2,str3,str4,…)

    • 作用:返回第N个字符串
    SELECT ELT(2,'字符串反转','sssss');
    

    在这里插入图片描述

    三. 日期和时间函数

    当前时间
    在这里插入图片描述

    1. CURDATE()、CURRENT_DATE()

    • 作用:将当前日期按照"YYYY-MM-DD"或者"YYYYMMDD"格式的值返回,具体格式根据函数用在字符串或是数字语境中而定

    2. CURRENT_TIMESTAMP()、LOCALTIME()、NOW()、SYSDATE()

    • 作用:这四个函数作用相同,返回当前日期和时间值,格式为"YYYY_MM-DD HH:MM:SS"或"YYYYMMDDHHMMSS",具体格式根据函数用在字符串或数字语境中而定
    SELECT CURRENT_TIMESTAMP()
    

    在这里插入图片描述

    SELECT LOCALTIME()
    

    在这里插入图片描述

    SELECT NOW()
    

    在这里插入图片描述

    SELECT SYSDATE()
    

    在这里插入图片描述

    3. UNIX_TIMESTAMP()、UNIX_TIMESTAMP(date)

    • 作用:前者返回一个格林尼治标准时间1970-01-01 00:00:00到现在的秒数,后者返回一个格林尼治标准时间1970-01-01 00:00:00到指定时间的秒数
    SELECT UNIX_TIMESTAMP()
    

    在这里插入图片描述

    4. FROM_UNIXTIME(date)

    • 作用:和UNIX_TIMESTAMP互为反函数,把UNIX时间戳转换为普通格式的时间

    5. UTC_DATE()和UTC_TIME()

    • 前者返回当前UTC(世界标准时间)日期值,其格式为"YYYY-MM-DD"或"YYYYMMDD",后者返回当前UTC时间值,其格式为"YYYY-MM-DD"或"YYYYMMDD"。具体使用哪种取决于函数用在字符串还是数字语境中
    SELECT UTC_DATE()
    

    在这里插入图片描述

    SELECT UTC_TIME()
    

    在这里插入图片描述

    6. MONTH(date)和MONTHNAME(date)

    • 作用:前者返回指定日期中的月份,后者返回指定日期中的月份的名称
    SELECT MONTH(NOW())
    

    在这里插入图片描述

    SELECT MONTHNAME(NOW())
    

    在这里插入图片描述

    7. DAYNAME(d)、DAYOFWEEK(d)、WEEKDAY(d)

    • 作用:DAYNAME(d)返回d对应的工作日的英文名称,如Sunday、Monday等;DAYOFWEEK(d)返回的对应一周中的索引,1表示周日、2表示周一;WEEKDAY(d)表示d对应的工作日索引,0表示周一,1表示周二

    8. WEEK(d)

    • 计算日期d是一年中的第几周
    SELECT WEEK(NOW())
    

    在这里插入图片描述

    9. DAYOFYEAR(d)、DAYOFMONTH(d)

    • 作用:前者返回d是一年中的第几天,后者返回d是一月中的第几天
    SELECT DAYOFYEAR(NOW())
    

    在这里插入图片描述

    SELECT DAYOFMONTH(NOW())
    

    在这里插入图片描述

    10. YEAR(date)、QUARTER(date)、MINUTE(time)、SECOND(time)

    • 作用: YEAR(date)返回指定日期对应的年份,范围是1970~2069;QUARTER(date)返回date对应一年中的季度,范围是1~4;MINUTE(time)返回time对应的分钟数,范围是0~59;SECOND(time)返回制定时间的秒值
    SELECT YEAR(NOW())
    

    在这里插入图片描述

    SELECT QUARTER(NOW())
    

    在这里插入图片描述

    SELECT MINUTE(NOW())
    

    在这里插入图片描述

    SELECT SECOND(NOW())
    

    在这里插入图片描述

    11. EXTRACE(type FROM date)

    • 作用:从日期中提取一部分,type可以是YEAR、YEAR_MONTH、DAY_HOUR、DAY_MICROSECOND、DAY_MINUTE、DAY_SECOND

    12. TIME_TO_SEC(time)

    • 作用:返回以转换为秒的time参数,转换公式为"3600小时 + 60分钟 + 秒"
    SELECT TIME_TO_SEC(NOW())
    

    在这里插入图片描述

    13. SEC_TO_TIME()

    • 作用:和TIME_TO_SEC(time)互为反函数,将秒值转换为时间格式
    SELECT SEC_TO_TIME(530)
    

    在这里插入图片描述

    14. DATE_ADD(date,INTERVAL expr type)、ADD_DATE(date,INTERVAL expr type)

    • 作用:返回将起始时间加上expr type之后的时间,比如DATE_ADD(‘2010-12-31 23:59:59’, INTERVAL 1 SECOND)表示的就是把第一个时间加1秒

    15. DATE_SUB(date,INTERVAL expr type)、SUBDATE(date,INTERVAL expr type)

    • 作用:返回将起始时间减去expr type之后的时间

    16. ADDTIME(date,expr)、SUBTIME(date,expr)

    • 作用:前者进行date的时间加操作,后者进行date的时间减操作

    四. 条件判断函数

    1. IF(expr,v1,v2)

    • 作用:如果expr是TRUE则返回v1,否则返回v2

    2. IFNULL(v1,v2)

    • 作用:如果v1不为NULL,则返回v1,否则返回v2

    3. CASE expr WHEN v1 THEN r1 [WHEN v2 THEN v2] [ELSE rn] END

    • 作用:如果expr等于某个vn,则返回对应位置THEN后面的结果,如果与所有值都不想等,则返回ELSE后面的rn

    五. 系统信息函数

    1. VERSION()

    • 作用:查看MySQL版本号
    SELECT VERSION()
    

    在这里插入图片描述

    2. CONNECTION_ID()

    • 作用:查看当前用户的连接数
    SELECT CONNECTION_ID()
    

    在这里插入图片描述

    3. USER()、CURRENT_USER()、SYSTEM_USER()、SESSION_USER()

    • 作用:查看当前被MySQL服务器验证的用户名和主机的组合,一般这几个函数的返回值是相同的
    SELECT USER()
    

    在这里插入图片描述

    SELECT CURRENT_USER()
    

    在这里插入图片描述

    SELECT SYSTEM_USER()
    

    在这里插入图片描述

    SELECT SESSION_USER()
    

    在这里插入图片描述

    4. CHARSET(str)

    • 作用:查看字符串str使用的字符集
    SELECT CHARSET(555)
    

    在这里插入图片描述

    5. COLLATION()

    • 作用:查看字符串排列方式
    
    SELECT COLLATION('sssfddsfds')
    

    在这里插入图片描述

    六. 加密函数

    1. PASSWORD(str)

    • 作用:从原明文密码str计算并返回加密后的字符串密码,注意这个函数的加密是单向的(不可逆),因此不应将它应用在个人的应用程序中而应该只在MySQL服务器的鉴定系统中使用
    SELECT PASSWORD('mima')
    

    在这里插入图片描述

    2. MD5(str)

    • 作用:为字符串算出一个MD5 128比特校验和,改值以32位十六进制数字的二进制字符串形式返回
    SELECT MD5('mima')
    

    在这里插入图片描述

    3. ENCODE(str, pswd_str)

    • 作用:使用pswd_str作为密码,加密str
    SELECT ENCODE('fdfdz','mima')
    

    在这里插入图片描述

    4. DECODE(crypt_str,pswd_str)

    • 作用:使用pswd_str作为密码,解密加密字符串crypt_str,crypt_str是由ENCODE函数返回的字符串
    SELECT DECODE('fdfdz','mima')
    

    在这里插入图片描述

    七. 其他函数

    1. FORMAT(x,n)

    • 作用:将数字x格式化,并以四舍五入的方式保留小数点后n位,结果以字符串形式返回
    SELECT FORMAT(446.454,2)
    

    在这里插入图片描述

    2. CONV(N,from_base,to_base)

    • 作用:不同进制数之间的转换,返回值为数值N的字符串表示,由from_base进制转换为to_base进制

    3. INET_ATON(expr)

    • 作用:给出一个作为字符串的网络地址的点地址表示,返回一个代表该地址数值的整数,地址可以使4或8比特

    4. INET_NTOA(expr)

    • 作用:给定一个数字网络地址(4或8比特),返回作为字符串的该地址的点地址表示

    5. BENCHMARK(count,expr)

    • 作用:重复执行count次表达式expr,它可以用于计算MySQL处理表达式的速度,结果值通常是0(0只是表示很快,并不是没有速度)。
    • 另一个作用是用它在MySQL客户端内部报告语句执行的时间

    6. CONVERT(str USING charset)

    • 作用:使用字符集charset表示字符串str

    更多用法还请参考:http://www.geezn.com/documents/gez/help/117555-1355219868404378.html

    在这里插入图片描述

    【SQL实战练习】

    • 题目来自互联网,建议每道题都在本地敲一遍巩固记忆 !

    创建数据库

    在这里插入图片描述

    创建表(并初始化数据)

    -- 学生表
    CREATE TABLE `student`(
    `s_id` VARCHAR(20),
    `s_name` VARCHAR(20) NOT NULL DEFAULT '',
    `s_birth` VARCHAR(20) NOT NULL DEFAULT '',
    `s_sex` VARCHAR(10) NOT NULL DEFAULT '',
    PRIMARY KEY(`s_id`)
    );
    -- 课程表
    CREATE TABLE `course`(
    `c_id` VARCHAR(20),
    `c_name` VARCHAR(20) NOT NULL DEFAULT '',
    `t_id` VARCHAR(20) NOT NULL,
    PRIMARY KEY(`c_id`)
    );
    -- 教师表
    CREATE TABLE `teacher`(
    `t_id` VARCHAR(20),
    `t_name` VARCHAR(20) NOT NULL DEFAULT '',
    PRIMARY KEY(`t_id`)
    );
    -- 成绩表
    CREATE TABLE `score`(
    `s_id` VARCHAR(20),
    `c_id` VARCHAR(20),
    `s_score` INT(3),
    PRIMARY KEY(`s_id`,`c_id`)
    );
    
    -- 插入学生表测试数据
    insert into student values('01' , '赵信' , '1990-01-01' , '男');
    insert into student values('02' , '德莱厄斯' , '1990-12-21' , '男');
    insert into student values('03' , '艾希' , '1990-05-20' , '男');
    insert into student values('04' , '德莱文' , '1990-08-06' , '男');
    insert into student values('05' , '俄洛依' , '1991-12-01' , '女');
    insert into student values('06' , '光辉女郎' , '1992-03-01' , '女');
    insert into student values('07' , '崔丝塔娜' , '1989-07-01' , '女');
    insert into student values('08' , '安妮' , '1990-01-20' , '女');
    -- 课程表测试数据
    insert into course values('01' , '语文' , '02');
    insert into course values('02' , '数学' , '01');
    insert into course values('03' , '英语' , '03');
    
    -- 教师表测试数据
    insert into teacher values('01' , '死亡歌颂者');
    insert into teacher values('02' , '流浪法师');
    insert into teacher values('03' , '邪恶小法师');
    
    -- 成绩表测试数据
    insert into score values('01' , '01' , 80);
    insert into score values('01' , '02' , 90);
    insert into score values('01' , '03' , 99);
    insert into score values('02' , '01' , 70);
    insert into score values('02' , '02' , 60);
    insert into score values('02' , '03' , 80);
    insert into score values('03' , '01' , 80);
    insert into score values('03' , '02' , 80);
    insert into score values('03' , '03' , 80);
    insert into score values('04' , '01' , 50);
    insert into score values('04' , '02' , 30);
    insert into score values('04' , '03' , 20);
    insert into score values('05' , '01' , 76);
    insert into score values('05' , '02' , 87);
    insert into score values('06' , '01' , 31);
    insert into score values('06' , '03' , 34);
    insert into score values('07' , '02' , 89);
    insert into score values('07' , '03' , 98);
    

    表结构

    • 这里建的表主要用于sql语句的练习,所以并没有遵守一些规范。下面让我们来看看相关的表结构吧

    学生表(student)

    在这里插入图片描述

    • s_id = 学生编号,s_name = 学生姓名,s_birth = 出生年月,s_sex = 学生性别

    课程表(course)

    在这里插入图片描述

    • c_id = 课程编号,c_name = 课程名称,t_id = 教师编号

    教师表(teacher)

    在这里插入图片描述

    • t_id = 教师编号,t_name = 教师姓名

    成绩表(score)

    在这里插入图片描述

    • s_id = 学生编号,c_id = 课程编号,s_score = 分数

    习题

    • 开始之前我们先来看看四张表中的数据。

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

    1. 查询"01"课程比"02"课程成绩高的学生的信息及课程分数

    SELECT
    	st.*,
    	sc.s_score AS '语文',
    	sc2.s_score '数学' 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.c_id = '01'
    	LEFT JOIN score sc2 ON sc2.s_id = st.s_id 
    	AND sc2.c_id = '02'
    

    在这里插入图片描述

    2. 查询"01"课程比"02"课程成绩低的学生的信息及课程分数

    SELECT
    	st.*,
    	s.s_score AS 数学,
    	s2.s_score AS 语文 
    FROM
    	student st
    	LEFT JOIN score s ON s.s_id = st.s_id 
    	AND s.c_id = '01'
    	LEFT JOIN score s2 ON s2.s_id = st.s_id 
    	AND s2.c_id = '02' 
    WHERE
    	s.s_score < s2.s_score
    

    在这里插入图片描述

    3. 查询平均成绩大于等于60分的同学的学生编号和学生姓名和平均成绩

    SELECT
    	st.s_id AS '学生编号',
    	st.s_name AS '学生姓名',
    	AVG( s.s_score ) AS avgScore 
    FROM
    	student st
    	LEFT JOIN score s ON st.s_id = s.s_id 
    GROUP BY
    	st.s_id 
    HAVING
    	avgScore >= 60
    

    在这里插入图片描述

    4. 查询平均成绩小于60分的同学的学生编号和学生姓名和平均成绩

    • (包括有成绩的和无成绩的)
    SELECT
    	st.s_id AS '学生编号',
    	st.s_name AS '学生姓名',(
    	CASE
    			
    			WHEN ROUND( AVG( sc.s_score ), 2 ) IS NULL THEN
    			0 ELSE ROUND( AVG( sc.s_score ), 2 ) 
    		END 
    		) 
    	FROM
    		student st
    		LEFT JOIN score sc ON st.s_id = sc.s_id 
    	GROUP BY
    		st.s_id 
    	HAVING
    	AVG( sc.s_score )< 60 
    	OR AVG( sc.s_score ) IS NULL
    

    在这里插入图片描述

    5. 查询所有同学的学生编号、学生姓名、选课总数、所有课程的总成绩

    SELECT
    	st.s_id AS '学生编号',
    	st.s_name AS '学生姓名',
    	COUNT( sc.c_id ) AS '选课总数',
    	sum( CASE WHEN sc.s_score IS NULL THEN 0 ELSE sc.s_score END ) AS '总成绩' 
    FROM
    	student st
    	LEFT JOIN score sc ON st.s_id = sc.s_id 
    GROUP BY
    	st.s_id
    

    在这里插入图片描述

    6. 查询"流"姓老师的数量

    SELECT COUNT(t_id) FROM teacher WHERE t_name LIKE '流%'
    

    在这里插入图片描述

    7. 查询学过"流浪法师"老师授课的同学的信息

    SELECT
    	st.* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id
    	LEFT JOIN course cs ON cs.c_id = sc.c_id
    	LEFT JOIN teacher tc ON tc.t_id = cs.t_id 
    	WHERE tc.t_name = '流浪法师'
    

    在这里插入图片描述

    8. 查询没学过"张三"老师授课的同学的信息

    -- 查询流浪法师教的课
    SELECT
    	cs.* 
    FROM
    	course cs
    	LEFT JOIN teacher tc ON tc.t_id = cs.t_id 
    WHERE
    	tc.t_name = '流浪法师'
    
    
    
    -- 查询有流浪法师课程成绩的学生id
    SELECT
    	sc.s_id 
    FROM
    	score sc 
    WHERE
    	sc.c_id IN (
    	SELECT
    		cs.c_id 
    	FROM
    		course cs
    		LEFT JOIN teacher tc ON tc.t_id = cs.t_id 
    	WHERE
    	tc.t_name = '流浪法师')
    
    
    
    -- 取反,查询没有学过流浪法师课程的同学信息
    SELECT
    	st.* 
    FROM
    	student st 
    WHERE
    	st.s_id NOT IN (
    	SELECT
    		sc.s_id 
    	FROM
    		score sc 
    	WHERE
    	sc.c_id IN ( SELECT cs.c_id FROM course cs LEFT JOIN teacher tc ON tc.t_id = cs.t_id WHERE tc.t_name = '流浪法师' ) 
    	)
    

    在这里插入图片描述

    9. 查询学过编号为"01"并且也学过编号为"02"的课程的同学的信息

    • 方法 1
    -- 查询学过编号为01课程的同学id
    SELECT
    	st.s_id 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id
    	INNER JOIN course cs ON cs.c_id = sc.c_id 
    	AND cs.c_id = '01';
    	
    	
    
    -- 查询学过编号为02课程的同学id
    SELECT
    	st2.s_id 
    FROM
    	student st2
    	INNER JOIN score sc2 ON sc2.s_id = st2.s_id
    	INNER JOIN course cs2 ON cs2.c_id = sc2.c_id 
    	AND cs2.c_id = '02';
    	
    	
    
    -- 查询学过编号为"01"并且也学过编号为"02"的课程的同学的信息
    SELECT
    	st.* 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id
    	INNER JOIN course cs ON cs.c_id = sc.c_id 
    	AND sc.c_id = '01' 
    WHERE
    	st.s_id IN (
    	SELECT
    		st2.s_id 
    	FROM
    		student st2
    		INNER JOIN score sc2 ON sc2.s_id = st2.s_id
    		INNER JOIN course cs2 ON cs2.c_id = sc2.c_id 
    		AND cs2.c_id = '02' 
    	);
    

    在这里插入图片描述

    • 方法 2
    SELECT
    	a.* 
    FROM
    	student a,
    	score b,
    	score c 
    WHERE
    	a.s_id = b.s_id 
    	AND a.s_id = c.s_id 
    	AND b.c_id = '01' 
    	AND c.c_id = '02';
    

    在这里插入图片描述

    10. 查询学过编号为"01"但是没有学过编号为"02"的课程的同学的信息

    SELECT
    	st.s_id 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id
    	INNER JOIN course cs ON cs.c_id = sc.c_id 
    	AND cs.c_id = '01' 
    WHERE
    	st.s_id NOT IN (
    	SELECT
    		st.s_id 
    	FROM
    		student st
    		INNER JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course cs ON cs.c_id = sc.c_id 
    		AND cs.c_id = '02' 
    	);
    

    在这里插入图片描述

    11. 查询没有学全所有课程的同学的信息

    • 方法 1
    SELECT
    	* 
    FROM
    	student 
    WHERE
    	s_id NOT IN (
    	SELECT
    		st.s_id 
    	FROM
    		student st
    		INNER JOIN score sc ON sc.s_id = st.s_id 
    		AND sc.c_id = '01' 
    	WHERE
    		st.s_id IN (
    		SELECT
    			st.s_id 
    		FROM
    			student st
    			INNER JOIN score sc ON sc.s_id = st.s_id 
    			AND sc.c_id = '02' 
    		WHERE
    			st.s_id 
    		) 
    		AND st.s_id IN (
    		SELECT
    			st.s_id 
    		FROM
    			student st
    			INNER JOIN score sc ON sc.s_id = st.s_id 
    			AND sc.c_id = '03' 
    		WHERE
    			st.s_id 
    		) 
    	);
    

    在这里插入图片描述

    • 方法 2
    SELECT
    	a.* 
    FROM
    	student a
    	LEFT JOIN score b ON a.s_id = b.s_id 
    GROUP BY
    	a.s_id 
    HAVING
    	COUNT( b.c_id ) != '3';
    

    在这里插入图片描述

    12. 查询至少有一门课与学号为"01"的同学所学相同的同学的信息

    SELECT DISTINCT
    	st.* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    WHERE
    	sc.c_id IN ( SELECT sc2.c_id FROM student st2 LEFT JOIN score sc2 ON sc2.s_id = st2.s_id WHERE st2.s_id = '01' );
    

    在这里插入图片描述

    13. 查询和"01"号的同学学习的课程完全相同的其他同学的信息

    SELECT
    	st.* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    HAVING
    	GROUP_CONCAT( sc.c_id )=(
    	SELECT
    		GROUP_CONCAT( sc2.c_id ) 
    	FROM
    		student st2
    		LEFT JOIN score sc2 ON sc2.s_id = st2.s_id 
    	WHERE
    		st2.s_id = '01' 
    	);
    

    在这里插入图片描述

    14. 查询没学过"邪恶小法师"老师讲授的任一门课程的学生姓名

    SELECT
    	* 
    FROM
    	student 
    WHERE
    	s_id NOT IN (
    	SELECT
    		sc.s_id 
    	FROM
    		score sc
    		INNER JOIN course cs ON cs.c_id = sc.c_id
    	INNER JOIN teacher t ON t.t_id = cs.t_id 
    	AND t.t_name = '邪恶小法师');
    

    在这里插入图片描述

    15. 查询两门及其以上不及格课程的同学的学号,姓名及其平均成绩

    SELECT
    	st.s_id AS '学号',
    	st.s_name AS '姓名',
    	AVG( sc.s_score ) AS '平均成绩' 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    WHERE
    	sc.s_id IN (
    	SELECT
    		sc.s_id 
    	FROM
    		score sc 
    	WHERE
    		sc.s_score < 60 
    		OR sc.s_score IS NULL 
    	GROUP BY
    		sc.s_id 
    	HAVING
    		COUNT( 1 )>= 2 
    	) 
    GROUP BY
    	st.s_id
    

    在这里插入图片描述

    16. 检索"01"课程分数小于60,按分数降序排列的学生信息

    SELECT
    	st.* 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.c_id = '01' 
    	AND sc.s_score < '60' 
    ORDER BY
    	sc.s_score DESC;
    	
    	
    SELECT
    	st.* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    WHERE
    	sc.c_id = '01' 
    	AND sc.s_score < '60' 
    ORDER BY
    	sc.s_score DESC;
    

    在这里插入图片描述

    17. 按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩

    • 方法 1
    SELECT
    	st.*,
    	AVG( sc4.s_score ) AS '平均分',
    	sc.s_score AS '语文',
    	sc2.s_score AS '数学',
    	sc3.s_score AS '英语' 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.c_id = '01'
    	LEFT JOIN score sc2 ON sc2.s_id = st.s_id 
    	AND sc2.c_id = '02'
    	LEFT JOIN score sc3 ON sc3.s_id = st.s_id 
    	AND sc3.c_id = '03'
    	LEFT JOIN score sc4 ON sc4.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    ORDER BY
    	AVG( sc4.s_score ) DESC;
    

    在这里插入图片描述

    • 方法 2
    SELECT
    	st.*,
    	( CASE WHEN AVG( sc4.s_score ) IS NULL THEN 0 ELSE AVG( sc4.s_score ) END ) AS '平均分',
    	( CASE WHEN sc.s_score IS NULL THEN 0 ELSE sc.s_score END ) AS '语文',
    	( CASE WHEN sc2.s_score IS NULL THEN 0 ELSE sc2.s_score END ) AS '数学',
    	( CASE WHEN sc3.s_score IS NULL THEN 0 ELSE sc3.s_score END ) AS '英语' 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.c_id = '01'
    	LEFT JOIN score sc2 ON sc2.s_id = st.s_id 
    	AND sc2.c_id = '02'
    	LEFT JOIN score sc3 ON sc3.s_id = st.s_id 
    	AND sc3.c_id = '03'
    	LEFT JOIN score sc4 ON sc4.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    ORDER BY
    	AVG( sc4.s_score ) DESC;
    

    在这里插入图片描述

    18. 查询各科成绩最高分、最低分和平均分:

    • 以如下形式显示:课程ID,课程name,最高分,最低分,平均分,及格率,中等率,优良率,优秀率
    • 及格为>=60,中等为:70-80,优良为:80-90,优秀为:>=90
    SELECT
    	cs.c_id,
    	cs.c_name,
    	MAX( sc1.s_score ) AS '最高分',
    	MIN( sc2.s_score ) AS '最低分',
    	AVG( sc3.s_score ) AS '平均分',
    	((
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			s_score >= 60 
    			AND c_id = cs.c_id 
    			)/(
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			c_id = cs.c_id 
    		)) AS '及格率',
    	((
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			s_score >= 70 
    			AND s_score < 80 
    			AND c_id = cs.c_id 
    			)/(
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			c_id = cs.c_id 
    		)) AS '中等率',
    	((
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			s_score >= 80 
    			AND s_score < 90 
    			AND c_id = cs.c_id 
    			)/(
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			c_id = cs.c_id 
    		)) AS '优良率',
    	((
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			s_score >= 90 
    			AND c_id = cs.c_id 
    			)/(
    		SELECT
    			COUNT( s_id ) 
    		FROM
    			score 
    		WHERE
    			c_id = cs.c_id 
    		)) AS '优秀率' 
    FROM
    	course cs
    	LEFT JOIN score sc1 ON sc1.c_id = cs.c_id
    	LEFT JOIN score sc2 ON sc2.c_id = cs.c_id
    	LEFT JOIN score sc3 ON sc3.c_id = cs.c_id 
    GROUP BY
    	cs.c_id;
    

    在这里插入图片描述

    19. 按各科成绩进行排序,并显示排名(实现不完全)

    • mysql没有rank函数
    • 加@score是为了防止用union all 后打乱了顺序
    SELECT
    	c1.s_id,
    	c1.c_id,
    	c1.c_name,
    	@score := c1.s_score,
    	@i := @i + 1 
    FROM
    	(
    	SELECT
    		c.c_name,
    		sc.* 
    	FROM
    		course c
    		LEFT JOIN score sc ON sc.c_id = c.c_id 
    	WHERE
    		c.c_id = "01" 
    	ORDER BY
    		sc.s_score DESC 
    	) c1,
    	( SELECT @i := 0 ) a UNION ALL
    SELECT
    	c2.s_id,
    	c2.c_id,
    	c2.c_name,
    	c2.s_score,
    	@ii := @ii + 1 
    FROM
    	(
    	SELECT
    		c.c_name,
    		sc.* 
    	FROM
    		course c
    		LEFT JOIN score sc ON sc.c_id = c.c_id 
    	WHERE
    		c.c_id = "02" 
    	ORDER BY
    		sc.s_score DESC 
    	) c2,
    	( SELECT @ii := 0 ) aa UNION ALL
    SELECT
    	c3.s_id,
    	c3.c_id,
    	c3.c_name,
    	c3.s_score,
    	@iii := @iii + 1 
    FROM
    	(
    	SELECT
    		c.c_name,
    		sc.* 
    	FROM
    		course c
    		LEFT JOIN score sc ON sc.c_id = c.c_id 
    	WHERE
    		c.c_id = "03" 
    	ORDER BY
    		sc.s_score DESC 
    	) c3;
    
    SET @iii = 0;
    

    在这里插入图片描述

    20. 查询学生的总成绩并进行排名

    SELECT
    	st.s_id,
    	st.s_name,
    	( CASE WHEN sum( sc.s_score ) IS NULL THEN 0 ELSE SUM( sc.s_score ) END ) 
    FROM
    	student st
    	LEFT JOIN score sc ON st.s_id = sc.s_id 
    GROUP BY
    	st.s_id 
    ORDER BY
    	SUM( sc.s_score ) DESC
    

    在这里插入图片描述

    21. 查询不同老师所教不同课程平均分从高到低显示

    SELECT
    	t.t_id,
    	t.t_name,
    	AVG( sc.s_score ) 
    FROM
    	teacher t
    	LEFT JOIN course c ON c.t_id = t.t_id
    	LEFT JOIN score sc ON sc.c_id = c.c_id 
    GROUP BY
    	t.t_id 
    ORDER BY
    	AVG( sc.s_score ) DESC
    

    在这里插入图片描述

    22. 查询所有课程的成绩第2名到第3名的学生信息及该课程成绩

    SELECT
    	a.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON sc.c_id = c.c_id 
    		AND c.c_id = '01' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 1,
    		2 
    	) a UNION ALL
    SELECT
    	b.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '02' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 1,
    		2 
    	) b UNION ALL
    SELECT
    	c.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '03' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 1,
    		2 
    	) c;
    

    在这里插入图片描述

    23. 统计各科成绩各分数段人数:课程编号,课程名称,[100-85],[85-70],[70-60],[0-60]及所占百分比

    SELECT
    	c.c_id,
    	c.c_name,
    	(
    	SELECT
    		COUNT( 1 ) 
    	FROM
    		score sc 
    	WHERE
    		sc.c_id = c.c_id 
    		AND sc.s_score <= 100 AND sc.s_score > 80 
    		)/(
    	SELECT
    		COUNT( 1 ) 
    	FROM
    		score sc 
    	WHERE
    		sc.c_id = c.c_id 
    	) AS '100-85',
    	((
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    			AND sc.s_score <= 85 AND sc.s_score > 70 
    			)/(
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    		)) AS '85-70',
    	((
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    			AND sc.s_score <= 70 AND sc.s_score > 60 
    			)/(
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    		)) AS '70-60',
    	((
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    			AND sc.s_score <= 60 AND sc.s_score >= 0 
    			)/(
    		SELECT
    			COUNT( 1 ) 
    		FROM
    			score sc 
    		WHERE
    			sc.c_id = c.c_id 
    		)) AS '85-70' 
    FROM
    	course c 
    ORDER BY
    	c.c_id 
    

    在这里插入图片描述

    24. 查询学生平均成绩及其名次

    SET @i = 0;
    SELECT
    	a.*,
    	@i := @i + 1 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		round( CASE WHEN AVG( sc.s_score ) IS NULL THEN 0 ELSE AVG( sc.s_score ) END, 2 ) AS agvScore 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id 
    	GROUP BY
    		st.s_id 
    	ORDER BY
    		agvScore DESC 
    	) a
    

    在这里插入图片描述

    25. 查询各科成绩前三名的记录

    SELECT
    	a.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '01' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    		3 
    	) a UNION ALL
    SELECT
    	b.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '02' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    		3 
    	) b UNION ALL
    SELECT
    	c.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_id,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '03' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    		3 
    	) c
    

    在这里插入图片描述

    26. 查询每门课程被选修的学生数

    SELECT
    	c.c_id,
    	c.c_name,
    	COUNT( 1 ) 
    FROM
    	course c
    	LEFT JOIN score sc ON sc.c_id = c.c_id
    	INNER JOIN student st ON st.s_id = c.c_id 
    GROUP BY
    	c.c_id
    

    在这里插入图片描述

    27. 查询出只有两门课程的全部学生的学号和姓名

    SELECT
    	st.s_id,
    	st.s_name 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id
    	INNER JOIN course c ON c.c_id = sc.c_id 
    GROUP BY
    	st.s_id 
    HAVING
    	COUNT( 1 ) = 2
    

    在这里插入图片描述

    28. 查询男生、女生人数

    SELECT s_sex, COUNT(1) FROM student GROUP BY s_sex
    

    在这里插入图片描述

    29. 查询名字中含有"德"字的学生信息

    SELECT * FROM student WHERE s_name LIKE '%德%'
    

    在这里插入图片描述

    30. 查询同名同性学生名单,并统计同名人数

    select st.s_name,st.s_sex,count(1) from student st group by st.s_name,st.s_sex having count(1)>1
    

    在这里插入图片描述

    31. 查询1990年出生的学生名单

    SELECT st.* FROM student st WHERE st.s_birth LIKE '1990%';
    

    在这里插入图片描述

    32. 查询每门课程的平均成绩,结果按平均成绩降序排列,平均成绩相同时,按课程编号升序排列

    SELECT
    	c.c_id,
    	c_name,
    	AVG( sc.s_score ) AS scoreAvg 
    FROM
    	course c
    	INNER JOIN score sc ON sc.c_id = c.c_id 
    GROUP BY
    	c.c_id 
    ORDER BY
    	scoreAvg DESC,
    	c.c_id ASC;
    

    在这里插入图片描述

    33. 查询平均成绩大于等于85的所有学生的学号、姓名和平均成绩

    SELECT
    	st.s_id,
    	st.s_name,
    	( CASE WHEN AVG( sc.s_score ) IS NULL THEN 0 ELSE AVG( sc.s_score ) END ) scoreAvg 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    HAVING
    	scoreAvg > '85';
    

    在这里插入图片描述

    34. 查询课程名称为"数学",且分数低于60的学生姓名和分数

    SELECT
    	* 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.s_score < 60
    	INNER JOIN course c ON c.c_id = sc.c_id 
    	AND c.c_name = '数学';
    

    在这里插入图片描述

    35. 查询所有学生的课程及分数情况

    SELECT
    	* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id
    	LEFT JOIN course c ON c.c_id = sc.c_id 
    ORDER BY
    	st.s_id,
    	c.c_name;
    

    在这里插入图片描述

    36. 查询任何一门课程成绩在70分以上的姓名、课程名称和分数

    SELECT
    	st.s_id,st.s_name,c.c_name,sc.s_score 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id
    	LEFT JOIN course c ON c.c_id = sc.c_id 
    WHERE
    	st.s_id IN (
    	SELECT
    		st2.s_id 
    	FROM
    		student st2
    		LEFT JOIN score sc2 ON sc2.s_id = st2.s_id 
    	GROUP BY
    		st2.s_id 
    	HAVING
    		MIN( sc2.s_score )>= 70 
    	ORDER BY
    	st2.s_id 
    	)
    

    在这里插入图片描述

    37. 查询不及格的课程

    SELECT
    	st.s_id,
    	c.c_name,
    	st.s_name,
    	sc.s_score 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.s_score < 60
    	INNER JOIN course c ON c.c_id = sc.c_id
    

    在这里插入图片描述

    38. 查询课程编号为01且课程成绩在80分以上的学生的学号和姓名

    SELECT
    	st.s_id,
    	st.s_name,
    	sc.s_score 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id 
    	AND sc.c_id = '01' 
    	AND sc.s_score >= 80;
    

    在这里插入图片描述

    39. 求每门课程的学生人数

    SELECT
    	c.c_id,
    	c.c_name,
    	COUNT( 1 ) 
    FROM
    	course c
    	INNER JOIN score sc ON sc.c_id = c.c_id 
    GROUP BY
    	c.c_id;
    

    在这里插入图片描述

    40. 查询选修"死亡歌颂者"老师所授课程的学生中,成绩最高的学生信息及其成绩

    SELECT
    	st.*,
    	sc.s_score 
    FROM
    	student st
    	INNER JOIN score sc ON sc.s_id = st.s_id
    	INNER JOIN course c ON c.c_id = sc.c_id
    	INNER JOIN teacher t ON t.t_id = c.t_id 
    	AND t.t_name = '死亡歌颂者' 
    ORDER BY
    	sc.s_score DESC 
    	LIMIT 0,1;
    

    在这里插入图片描述

    41. 查询不同课程成绩相同的学生的学生编号、课程编号、学生成绩

    SELECT
    	st.s_id,
    	st.s_name,
    	sc.c_id,
    	sc.s_score 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id
    	LEFT JOIN course c ON c.c_id = sc.c_id 
    WHERE
    	(
    	SELECT
    		COUNT( 1 ) 
    	FROM
    		student st2
    		LEFT JOIN score sc2 ON sc2.s_id = st2.s_id
    		LEFT JOIN course c2 ON c2.c_id = sc2.c_id 
    	WHERE
    		sc.s_score = sc2.s_score 
    	AND c.c_id != c2.c_id 
    	)>1;
    

    在这里插入图片描述

    42. 查询每门功成绩最好的前两名

    SELECT
    	a.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '01' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    		2 
    	) a UNION ALL
    SELECT
    	b.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '02' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    		2 
    	) b UNION ALL
    SELECT
    	c.* 
    FROM
    	(
    	SELECT
    		st.s_id,
    		st.s_name,
    		c.c_name,
    		sc.s_score 
    	FROM
    		student st
    		LEFT JOIN score sc ON sc.s_id = st.s_id
    		INNER JOIN course c ON c.c_id = sc.c_id 
    		AND c.c_id = '03' 
    	ORDER BY
    		sc.s_score DESC 
    		LIMIT 0,
    	2 
    	) c;
    

    在这里插入图片描述

    写法 2

    SELECT
    	a.s_id,
    	a.c_id,
    	a.s_score 
    FROM
    	score a 
    WHERE
    	( SELECT COUNT( 1 ) FROM score b WHERE b.c_id = a.c_id AND b.s_score > a.s_score ) <= 2 
    ORDER BY
    	a.c_id;
    

    在这里插入图片描述

    43. 统计每门课程的学生选修人数(超过5人的课程才统计)

    • 要求输出课程号和选修人数,查询结果按人数降序排列,若人数相同,按课程号升序排列
    SELECT
    	c.c_id,
    	COUNT( 1 ) 
    FROM
    	score sc
    	LEFT JOIN course c ON c.c_id = sc.c_id 
    GROUP BY
    	c.c_id 
    HAVING
    	COUNT( 1 ) > 5 
    ORDER BY
    	COUNT( 1 ) DESC,
    	c.c_id ASC;
    

    在这里插入图片描述

    44. 检索至少选修两门课程的学生学号

    SELECT
    	st.s_id 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    HAVING
    	COUNT( 1 )>= 2;
    

    在这里插入图片描述

    45. 查询选修了全部课程的学生信息

    SELECT
    	st.* 
    FROM
    	student st
    	LEFT JOIN score sc ON sc.s_id = st.s_id 
    GROUP BY
    	st.s_id 
    HAVING
    	COUNT( 1 )=(
    	SELECT
    		COUNT( 1 ) 
    FROM
    	course)
    

    在这里插入图片描述

    46. 查询各学生的年龄

    SELECT
    	st.*,
    	TIMESTAMPDIFF(
    		YEAR,
    		st.s_birth,
    	NOW()) 
    FROM
    	student st
    

    在这里插入图片描述

    47. 查询本周过生日的学生

    SELECT
    	st.* 
    FROM
    	student st 
    WHERE
    	WEEK (
    	NOW())+ 1 = WEEK (
    	DATE_FORMAT( st.s_birth, '%Y%m%d' ))
    

    在这里插入图片描述

    48. 查询下周过生日的学生

    SELECT
    	st.* 
    FROM
    	student st 
    WHERE
    	WEEK (
    		NOW())+ 1 = WEEK (
    	DATE_FORMAT( st.s_birth, '%Y%m%d' ));
    

    在这里插入图片描述

    49. 查询本月过生日的学生

    SELECT
    	st.* 
    FROM
    	student st 
    WHERE
    	MONTH (
    	NOW())= MONTH (
    	DATE_FORMAT( st.s_birth, '%Y%m%d' ));
    

    在这里插入图片描述

    50. 查询下月过生日的学生

    SELECT
    	st.* 
    FROM
    	student st 
    WHERE
    	MONTH (
    		TIMESTAMPADD(
    			MONTH,
    			1,
    		NOW()))= MONTH (
    	DATE_FORMAT( st.s_birth, '%Y%m%d' ));
    

    在这里插入图片描述


    【阿里巴巴开发手册】

    在这里插入图片描述

    点击预览在线版: 阿里巴巴开发手册


    内容偏向基础适合各个阶段人员的学习与巩固,如果对您还有些帮助希望给博主点个赞在这里插入图片描述支持一下,感谢!

    展开全文
  • 这个是面试一家给我的笔试题,这个题我是用双重... 哈希的思路拓展,重排数组:把扫描的每个数字(如数字m)放其对应下标(m下标)的位置上,若同一位置有重复,则说明该数字重复。 题目上面还了数组的范围:
  • 神经网络实现手写数字识别(MNIST)

    万次阅读 多人点赞 2017-05-10 18:20:42
    、缘起 原本想沿着 传统递归算法实现迷宫游戏 ——&amp;amp;amp;amp;gt; 遗传算法实现迷宫游戏 ——&...从小至蚂蚁(没有查具体数目,有的说蚂蚁大脑有25个神经细胞,也有说是50个),大至大象...
  • 12岁男孩个暑假卖NFT「表情包」赚了250

    千次阅读 多人点赞 2021-08-31 15:26:53
    梦晨 发自 凹非寺量子位 报道 | 公众号 QbitAI12岁男孩在暑假里画了系列画,在区块链上以NFT的形式出售,赚的虚拟货币价值相当于250人民币!这些画倒也不是什么精致的艺术品...
  • 为什么这篇文章 我2021年6月13号下第篇Python的系列专栏算起,陆续更新了二十七篇Python系列文章。在此感谢读者朋友们的支持和阅读,特别感谢一键三连的小伙伴。 本专栏起名【Python入门精通】,主要...
  • 作者: 龙心尘&&寒小阳 时间:2015年12月。 出处: ...声明:版权所有,转载请联系作者并注明出处,谢谢。...就像刚开始学游泳,你在只在岸上比划堆规定动作还不如先跳水里熟悉水性学习来得快。以我们学习“机
  • 一年二年面试java可能问的问题

    千次阅读 2018-09-28 15:48:07
    对于一年经验 会问什么问题呢 根据在qq群里大神聊天 所记录一下 ! 数据库问题 --内连接SELECT Students.ID,Students.Name,Majors.Name AS MajorNameFROM Students INNER JOIN Majors ON Students.MajorID ...
  • 可以后往前遍历,遇到的是09的数字,就乘以前位的单位,遇到新的单位(十百千)就替换成数字供下数字用。 三、举例 五百四十三 1. 三-->3 3 <10 : total = 3 2. 十-->10, 10 ≥10,且不为0 :...
  • 使用阿里云rds for MySQL数据库(就是MySQL5.6版本),有个用户上网记录6个月的数据量近2000,保留最近一年的数据量达到4000,查询速度极慢,日常卡死。严重影响业务。 问题前提:老系统,当时设计系统的人...
  • 使用cnn做手写数字识别“Artificial Intelligence is a branch of science which offers with helping machines discover solutions to complex problems in a greater human-like-fashion” (Sridhar, 2020)....
  • 如何快速成为个运维工程师

    万次阅读 多人点赞 2018-05-24 16:08:49
    比如XX路XX号XX大厦XX楼XX室,你写到xx号,送快递的会给你堆院子里,你写到xx楼,送快递的会给你送电梯口,你写到xx室,他们才会给你搬室内。因为送货的都是服务器厂商找的,你因为这个事情去联系厂商修改送货...
  • 我给鸿星尔克个720°看鞋展厅

    万次阅读 多人点赞 2021-07-28 11:18:25
    最近因为鸿星尔克给河南捐了5000物资,真的是看哭了很多的网友,普通一家公司捐款5000可能不会有这样的共情,但是看了鸿星尔克的背景之后,发现真的是令人心酸。鸿星尔克2020年的营收是28亿,但是利润却是亏损2...
  • 在20世纪40年代和50年代,来自不同领域(数学,心理学,工程学,经济学和政治学)的批科学家开始探讨制造人工大脑的可能性。1956年,人工智能被确立为一门学科。 控制论与早期神经网络 最初的人工智能研究是30年...
  • 数字IC小白起步()

    千次阅读 多人点赞 2018-09-27 10:49:16
    然而在后来的工作过程中,我认识了很多牛人,也他们身上学了很多,从中总结了个IC设计工程师需要具备的知识架构,想跟大家分享一下。 I. 技能清单 作为个真正合格的数字IC设计工程师,你永远都需要去不断...
  • 英语中数字表达总结

    千次阅读 2019-01-22 12:17:07
    对于英语中千、、亿的表达法,始终是我英语路上的个痛点,今天就来好好总结一下这东西的规律。 句话总结 阅读和理解层面(99%的使用情况):thousand后面跟 3 个 0 ,million后面跟 6 个 0 , billion 后面...
  • 编写函数,将数字(Integer)转换成中文返回。
  • 01,个普本学生的三年学习历程

    万次阅读 多人点赞 2020-04-25 14:56:11
    我是17年开始认识IT这个行业的,至今也快将近三年了吧。...这片文章我断断续续了有些时间了,因为我总是在想这三年我到底学了什么?现在是时候该做个总结了,所以该文并没有什么技术,有的只是个人小小经验。
  • 人工智能时代,所需要了解人工智能的基本常识

    万次阅读 多人点赞 2018-12-10 22:49:44
    国内对于人工智能的讨论大多是不成体系的碎片式,很难从中深入了解人工智能的发展脉络和技术体系,也很难有实际... 、概述  近几年各界对人工智能的兴趣激增,自2011年以来,开发与人工智能相关的产品和技术...
  • 大数据经过10年发展,现在已经个重要的分水岭阶段:通用性和兼容性能力成为大数据发展主流,运行的稳定可靠和使用的简捷、易开发、易维护成为产品发展的驱动力,而这正是 Hadoop/Spark 这类积木式模块框架无法...
  • mysql面试题

    千次阅读 2019-09-23 12:28:36
    最全MySQL面试题和答案 Mysql 的存储引擎,myisam和innodb的区别。 答: 1.MyISAM 是非事务的存储引擎,适合用于频繁查询的应用。表锁,不会出现死锁,适合小数据,小并发。...数据类型有哪些     ...
  • 用python写一个简单的中文搜索引擎

    万次阅读 多人点赞 2016-12-09 23:10:50
    自己代码实现个简陋的中文的搜索引擎,作为个小练习。 搜索范围限定在某个新闻网站内部,和百度site:(指定网址)的功能类似。把爬虫和解析的代码改改,也可以用于其他的搜索场合。 使用的编程语言为python。
  •  英语中千以上数字的表达方法是以三位数为单元,低billion,million,thousand而依次 读出的。因此我们在听时,可以三位数三位数地记录。试比较下面的对话:  A:Can you tell me exactly the ...
  • 信息化,肯定离不开对CIO的...CIO是熬出来的,CIO要耐得住寂寞,背得起“黑锅”,但行好事,莫问前程,最后带领团队从一个成功走向另外个成功。用句主席诗词共勉,牢骚太盛防断防肠断,风物长宜放眼量。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 89,629
精华内容 35,851
关键字:

从一写到一万数字表