详解.net中IL语言

什么是IL语言

中间语言,又称(IL语言)。充当Clr与.net 平台的中间语言,比如用C#编写程序,编译器首先是把C#代码转译成IL语言,最终由Clr解释执行,下面我们学习下IL语言。

如何读懂IL语言

  • 写一个helloworld的.net 程序,编译运行完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    		static void Main(string[] args)
    {
    Console.WriteLine("hello world");
    }
    ```

    - 使用ildasm.exe(C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools)反编译代码,得到IL代码如下:

    ```IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint
    // 代码大小 13 (0xd)
    .maxstack 8
    IL_0000: nop
    IL_0001: ldstr "hello world"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret
    } // end of method Program::Main
    ```

    - 查找对应的[指令表](http://blog.csdn.net/xiaouncle/article/details/71248830),来确定对应的含义


    <table class="table">
    <thead>
    <tr>
    <th>指令名称</th>
    <th>说明</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Ldstr</td>
    <td>推送对元数据中存储的字符串的新对象引用。</td>
    </tr>
    <tr>
    <td>Nop</td>
    <td>如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。</td>
    </tr>
    <tr>
    <td>Call</td>
    <td>调用由传递的方法说明符指示的方法。</td>
    </tr>
    <tr>
    <td>Ret</td>
    <td>从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。</td>
    </tr>
    </tbody>
    </table>

    - 其它几个名词的的解释

    **hidebysig**: 与之对就的是hidebyname,这个是确定使用方法的签名还是使用方法的名称来确定调用哪个方法.


    - 整个的IL语言解释

    ```IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint //代码入口
    // 代码大小 13 (0xd)
    .maxstack 8 //整个程序的堆栈大小
    IL_0000: nop //无实在意义
    IL_0001: ldstr "hello world" //定义字符
    IL_0006: call void [mscorlib]System.Console::WriteLine(string) //调用WriteLine变量
    IL_000b: nop
    IL_000c: ret //返回
    } // end of method Program::Main
    ```


    ### 更复杂的Demo

    - 添加编写如下C#代码:

    ``` C#
    class Program
    {
    static void Main(string[] args)
    {
    var a = 0;
    var b = 1;
    var c = Add(a, b);
    Console.WriteLine(c.ToString());
    }

    public static int Add(int x,int y)
    {
    return x + y;
    }
    }

    ```

    - 生成相关的IL代码及解释

    ``` IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint
    // 代码大小 27 (0x1b)
    .maxstack 2
    .locals init ([0] int32 a,
    [1] int32 b,
    [2] int32 c) //定义3个变量
    IL_0000: nop
    IL_0001: ldc.i4.0 //将整数值 0 作为 int32 推送到计算堆栈上。
    IL_0002: stloc.0 //从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
    IL_0003: ldc.i4.1 //将整数值 1 作为 int32 推送到计算堆栈上。
    IL_0004: stloc.1 //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
    IL_0005: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上,这里指a。
    IL_0006: ldloc.1 //将索引 1 处的局部变量加载到计算堆栈上,这里指b。
    IL_0007: call int32 ILTest.Program::Add(int32,
    int32) //调用Add方法
    IL_000c: stloc.2 //将索引 2 处的局部变量加载到计算堆栈上,这里指c。
    IL_000d: ldloca.s c //将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
    IL_000f: call instance string [mscorlib]System.Int32::ToString()
    IL_0014: call void [mscorlib]System.Console::WriteLine(string)
    IL_0019: nop
    IL_001a: ret
    } // end of method Program::Main

    ```

    Add方法:

    ``` IL
    .method public hidebysig static int32 Add(int32 x,int32 y) cil managed
    {
    // 代码大小 9 (0x9)
    .maxstack 2
    .locals init ([0] int32 V_0) //创建一个V_0的局部变量
    IL_0000: nop
    IL_0001: ldarg.0 //将索引为 0 的参数加载到计算堆栈上。
    IL_0002: ldarg.1 //将索引为 1 的参数加载到计算堆栈上。
    IL_0003: add //将两个值相加并将结果推送到计算堆栈上。
    IL_0004: stloc.0
    IL_0005: br.s IL_0007 //无条件地将控制转移到目标指令(短格式)
    IL_0007: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上。
    IL_0008: ret
    } // end of method Program::Add

服务器CPU居高不下--解决问题历程

基本的概述

在一个服务器的集群上面,服务器的CPU长时间居高不下,响应的时间也一直很慢,即使扩容了服务器CPU的下降效果也不是很明显。

对于CPU过高的原因,可以总结到以下原因:

  • 太多的循环或者死循环

  • 加载了过多的数据,导致产生了很多的大对象

  • 产生了过多的对象,GC回收过于频繁(如:字符串拼接)

对于上面的情况,难点不是优化代码,难点在于定位到问题的所在,下面我们就用Dump抓包的方式来定位到问题的所在。介绍这个内容之前,我们要先回顾下.Net中垃圾回收的基础知识和一个工具的准备。


基础知识


垃圾回收触发条件

  • 代码显示调用System.GC的静态方法

  • windows报告低内存情况

  • CLR正在卸载AppDoamin

  • CLR正在关闭

大对象垃圾回收

CLR将对象分为大对象和小对象,认为大于85000字节或者更大的字节是大对象,CLR用不同的方式来对待大对象和小对象:

  • 大对象不是在小对象的地址空间分配,而是在进程地址空间和其他地方分配

  • GC不会压缩大对象,在内存中移动他们的代价过高,但这样会造成地址空间的碎片化,以至于会抛出OutOfMemeryException 异常。

  • 大对象总是在第二代回收。

工具准备

  1. 下载[windbg文件](dbg_amd64_6.12.2.633.msi)

  2. 相关DLL准备clr.dll和sos.dll,(都在对应.Net版本安装目录下面,我的安装目录在C:\Windows\Microsoft.NET\Framework64\v4.0.30319)

  3. 一个cpu运行的较高的时期的DUMP文件(下面会说如何获取)

  4. 准备测试代码,此处为了演示方便,简单了写了一个有潜在问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public  class Common
{
public static List<string> GetList()
{
var list=new List<string>();
for (int i = 0; i < 10000; i++)
{
list.Add(i.ToString());
}
return list;
}


public static string GetString(List<string> list)
{
var str = "";
foreach (var l in list)
{
str += string.Format("'{0}',", l);
}
if (str.Length > 0)
{
str.Remove(str.Length - 1);
}
return str;
}
}

我们知道在字符串的拼接的时候,每一个字符串都是一个对象,拼接后又产生了一个新对象,所以在GetString这个方法中会有大量的GC操作,下面我们就调用下这个代码,看下CPU的情况,为了模拟并发情况,我们开多个标签,每个标签每1s秒中刷新一次。

CPU

抓取Dump

在任务管理器中选择应用程序池对应的w3wp.exe,右击–>创建转储文件。创建完成后,会提示出指定的路径

taskManger

taskManger

根据上面的步骤,我们准备我们分析的文件如下:

dumpfile

分析Dump

  • 打开windbg,加载对应的dump文件

    dumpOpen
    dumpOpen

  • 配置Sysmbol,添加”cachec:\mysymbol;srvhttp://msdl.microsoft.com/download/symbols
    dumpOpen
    dumpOpen

  • load sos.dll和clr.dll,命令如下:

    .load D:\windbg\sos.dll 
    .load D:\windbg\clr.dll
    
  • 运行命令!threadpool 显示有关托管线程池的信息,其它一些SOS 调试扩展命令.

    dumpOpen

  • 运行!runaway 查询cpu占用时长比较长的几个线程Id

    dumpOpen

  • 运行~22s (进入线程查看),kb(查看对应的调用)

    dumpOpen

  • 运行~* kb 查看所有线程的堆栈调用

    dumpOpen

  • 在上面搜索GC和大对象出现的线程 (ctrl+f搜索:GarbageCollectGeneration和allocate_large_object )

    dumpOpen

  • 可以看到定位触发GC的线程是31号线程

  • 运行命令~31s 进入31线程,再运行!clrstack查看堆栈调用,最终可以定位到出问题的代码,是由于字符串的拼接导致大量的对象产生,从而触发了GC。

    dumpOpen

多线程如何排队执行

场景

有一个这样场景,程序会有一个非常耗时的操作,但要求耗时的操作完成后,再顺序的执行一个不耗时的操作,而且这个程序的调用,可能存在同时调用的情况。

具体的模型如下:

moxing

从Start开始触发了5个线程,经过一个longTimeJob同时执行,我们不关心longJob的执行时间和先后顺序,根据Start的先后顺序来执行一个ShortJob。下面我们用代码来模拟上面的过程。

举例说明:有ABCD 4个线程,进入的顺序也是ABCD,A耗时3s,B耗时7s,C耗时1s,D耗时3s. 所以如果当4个线程都同时开始执行时,完成的先后顺序为 CADB,但我们要求的顺序是ABCD,也就是说C要等待AB执行完后,才能继续后续的工作。

我们可以用请求bing搜索来模拟longTimeJob,根据传入的序列来决定请求多少次,主要模拟方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

private static async Task Test()
{
var arry = new[]
{
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
};
var listTask = new List<Task<int>>();
foreach (var i in arry)
{
var i1 = i;
var task = Task.Run(() => DoJob(i1));

listTask.Add(task);
}

foreach (var task in listTask)
{
Console.WriteLine("输出-->:" + await task);//
}
}

public static Task<int> DoJob(int o)
{
return Task.Run(() =>
{
DoLongTimeThing(o);
return o;
});
}

public static void DoLongTimeThing(int i)
{
Console.WriteLine("执行-->:" + i);
for (int j = 0; j < i; j++)
{
HttpGet("http://cn.bing.com/");
}

Console.WriteLine("执行完毕-->:" + i);
}

public static string HttpGet(string url)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";

HttpWebResponse response = (HttpWebResponse)request.GetResponse();
StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
string content = reader.ReadToEnd();
return content;
}
catch (Exception e)
{
return e.Message;
}

}



执行结果:

taskjob

上面的代码大概能解决我们的问题,有一个问题,对于客户的调用我们无法形成一个List,而且list是线程安全的,所以针对上述的方法在实际的业务场景中无法使用。

新思路

我们无法实现一个有序的Task列表,如果换一个角度考虑,当一个任务形成的时间,同时生成一个对应的HashCode,对HashCode进行一个队列的入队操作,当执行完成longTimeJob后,判断是不是队列的第一个Task的HashCode,如果是则执行,如果不是则继续等待,切换线程。 具体如下思路如下图:

queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

public static Queue<string> Queue = new Queue<string>();
static void Main(string[] args)
{
var arry = new[]
{
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
};

foreach (var i in arry)
{
Console.WriteLine("进入Job顺序-->:" + i);
Test(i);
}
Console.ReadKey();
}

public static void Test(int i)
{
var taskId = Guid.NewGuid().ToString();
Queue.Enqueue(taskId);
Task.Factory.StartNew(DoJob, new object[] { i, taskId });
}
public static void DoJob(object o)
{
var oArry = (object[])o;
var n = (int)oArry[0];
var currId = oArry[1].ToString();

DoLongTimeThing(n);//

while (currId != Queue.Peek())
{
Thread.Sleep(1);//等线程切换
}

Console.WriteLine("DoShortJob输出-->:" + n);//
//请求数据库
Queue.Dequeue();
}
public static void DoLongTimeThing(int i)
{
Console.WriteLine("LongTimeJob执行-->:" + i);
for (int j = 0; j < i; j++)
{
HttpGet("http://cn.bing.com/");
}
Console.WriteLine("LongTimeJob执行完毕-->:" + i);
}


public static string HttpGet(string url)
{
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";

HttpWebResponse response = (HttpWebResponse)request.GetResponse();
StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
string content = reader.ReadToEnd();
return content;
}
catch (Exception e)
{
return e.Message;
}

}


运行结果如下:

queue

虽然执行结果看起来很乱,但仔细比对可以发现最终的DoShortTime是按顺序执行的。

IIS执行原理

服务器的监听(IIS6.0+版本)

  1. 当请求到达服务器时,请求最终会到达TCPIP.SYS驱动程序,TCPIP.SYS将请求转发给HTTP.SYS网络驱动程序的请求队列中(可以理解为专门处理http请求的进程),当然在处理请求的过程中,HTTP.SYS进程会维护一个配置表用缓存请求的url和和应用程序池对应的关系。

  2. 当一个http请求被捕获到,HTTP.SYS会读取配置表,如果对应的应用程序没有启动,则HTTP.SYS会启动IIS相对应的应用程序。具体运行机制可以理解成为:

HTTP.SYS

HTTP.SYS是TCP之上的一个网络驱动程序,因此,HTTP.SYS不再属于IIS(这里说的IIS都是IIS6.0+版本,下文如果不特殊指明,默认为IIS6.0+版本),它已经从IIS中独立了出来。 Http.Sys独立有以下几个优点:

  • 可靠性: HTTP.SYS运行在内核模式下,作为操作系统的驱动程序运行。因此,HTTP.SYS不会受到用户代码的影响,它始终处于稳定运行状态,对用户的http请求进行监听,并及时作出反应。

  • 高性能: 从用户发送http请求到系统返回响应结果的这一过程都是HTTP.SYS在内核模式下完成的。不需要在内核模式和用户模式下进行切换,这样就极大地节省了系统资源,提高了请求的响应速度。

IIS处理

W3SVC

  1. W3SVC服务是一个独立运行的程序,寄宿在svchost.exe进程中,负责用户的参数监视和重新启动应用池的工作。 当一个请求进入HTTP.SYS的队列中,会通知W3SVC服务根据IIS中的配置去创建对应的应用进程,进行处理。

W3WP.exe

  1. 当HTTP.SYS把请求传递给IIS时候,W3SVC会启动对应的应用程序池
  2. 当用户请求的是静态文件,如:HTML和图片等,IIS会直接读取文件内容,转成二进制文件流,返回给HTTP.SYS。
  3. 当请求非静态文件,如:.aspx。
  • 3-1. w3wp.exe会根据IIS中ISAPI扩展读取对应的处理的Dll,用asp.net举例:当用户访问的网站是asp.net平台,则 类型是.cshtml和.aspx文件类型。根据配置w3wp.exe会加载aspnet_isapi.dll(简称是ISAPI).

IIS中应用程序的映射:

IIS中处理流程:

  • 3-2. 当ISAPI加载后,会启动一个ASP.NET的工作进程,把信息的控制权交给Asp.Net来处理。此处请求的处理由IIS交给了asp.net的程序。

    基于对上面的说明,可以把IIS的处理过程理解表示如下图:

说到这里,把IIS请求的流程简单的做了说明,后面的工作就由Asp.Net去完成了。

.Net程序的运行过程

说到Asp.Net的运行,不得不先说下.Net的运行机制(算是为后面的文章做一个铺垫)。
在vs中写了一段C#代码(或者其它.net平台的语言,此处简单的用C#来说明) ,编译器会把代码转译成IL的中间语言程序。当程序运行时,系统调用jit编译器,把中间语言编译成对应的cpu指令,等待cpu的最终调用。具体过程如下:

托管和非托管

  • 定义

    托管的概念是在.net框架诞生后出现的。用比较通俗的话解释就是运行在.net框架下,并受.net框架管理的应 用或其他组件称为托管的,反之为非托管的。

  • 区别

    1、托管代码是一种中间语言,运行在CLR上;非托管代码被编译为机器码,运行在机器上。
    2、托管代码独立于平台和语言,能更好的实现不同语言平台之间的兼容;非托管代码依赖于平台和语言。
    3、托管代码可享受CLR提供的服务(如安全检测、垃圾回收等),不需要自己完成这些操作;非托管代码需要自己提供安全检测、垃圾回收等操作。

  • 性能
    对于这个问题,首先澄清.net中的JIT是不同Java中的JVM的(JVM是一个Interpreter,在运行时读取IL汇编代码,然后模拟成x86代码),在.Net中使用的是一种更高级的技术,在程序首次加载的时候,JIT是把代码编译成本地指令(这也就是为什么.Net程序首次运行很慢的原因,但你的程序不可能只跑一次,尤其是在服务器上面的程序!),.NET程序经JIT转换后与非托管程序运行一样了,直接由CPU执行。
    但对于JIT来说,恰恰由于是即时编译,对当前的环境认识的比非托管更为深刻(包括当前的CPU最新的指令),在编译时可以进行优化。而非托管代码,在编译的时候要保证兼容性,所以只能使用最通用的cpu指令(公共的CPU指令),所以我个人认为,.Net在执行的效率上更具有优势。
    非托管编译运行过程

托管代码编译运行过程

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×