zoukankan      html  css  js  c++  java
  • linux中的dup()系统调用

    参考1:http://www.blogjava.net/lihao336/archive/2011/12/13/366231.html

    在linux纷繁复杂的内核代码中,sys_dup()的代码也许称得上是最简单的之一了,但是就是这么一个简单的系统调用,却成就了unix/linux系统最著名的一个特性:输入/输出重定向。
        sys_dup()的主要工作就是用来“复制”一个打开的文件号,使两个文件号都指向同一个文件。既然说简单,我们就首先来看一下它的代码(定义在fs/fcntl.c中):

     1 asmlinkage long sys_dup(unsigned int fildes)
     2 {
     3     int ret = -EBADF;
     4     struct file * file = fget(fildes);
     5 
     6     if (file)
     7         ret = dupfd(file, 0);
     8     return ret;
     9 }
    10 


    而sys_dup()的主体是dupfd()(定义在同一个文件中):

     1 static int dupfd(struct file *file, int start)
     2 {
     3     struct files_struct * files = current->files;
     4     int ret;
     5 
     6     ret = locate_fd(files, file, start);
     7     if (ret < 0)
     8         goto out_putf;
     9     allocate_fd(files, file, ret);
    10     return ret;
    11 
    12 out_putf:
    13     write_unlock(&files->file_lock);
    14     fput(file);
    15     return ret;
    16 }
    17 


    注:dup和dup2的原型如下:
    #include <unistd.h>

    int dup(int file_descriptor);
    int dup2(int file_descriptor1, int file_descriptor2)
    dup返回的文件描述符总是取最小的可用值
    dup2返回的文件描述符或者与file_descriptor2相同,或者是第一个大于该参数的可用值。


        而这么一个简单的系统调用是如何完成重定向这个艰巨的任务的呢?我们不妨先看个例子。
        当我们在shell下输入如下命令:“echo hello!”,这条命令要求shell进程执行一个可执行文件echo,参数为“hello!”。当shell接收到命令之后,先找到 bin/echo,然后fork()出一个子进程让他执行bin/echo,并将参数传递给它,而这个进程从shell继承了三个标准文件,即标准输入 (stdin),标准输出(stdout)和标准出错信息(stderr),他们三个的文件号分别为0、1、2。而至于echo进程的工作很简单,就是将参数“hello!”写到标准输出文件中去,通常都是我们的显示器上。但是如果我们将命令改成“echo hello! > foo”,则在执行时输出将会被重定向到磁盘文件foo中(注:重定向于文件描述符有关)。我们假定在此之前该shell进程只有三个标准文件打开,文件号分别为0、1、2,以上命令行将按如下序列执行:
        (1) 打开或创建磁盘文件foo,如果foo中原来有内容,则清除原来内容,其文件号为3。
        (2) 通过dup()复制文件stdout,即将文件号1出的file结构指针复制到文件号4处,目的是将stdout的file指针暂时保存一下
        (3) 关闭stdout,即1号文件,但是由于4号文件对stdout也同时有个引用,所以stdout文件并未真正关闭,只是腾出1号文件号位置。
        (4) 通过dup(),复制3号文件(即磁盘文件foo),由于1号文件关闭,其位置空缺,故3号文件被复制到1号,即进程中原来指向stdout的指针指向了foo。
        (5) 通过系统调用fork()和exec()创建子进程并执行echo,子进程在执行echo前夕关闭3号和4号文件,只留下0、1、2三个文件,请注意,这 时的1号文件已经不是stdout而是磁盘文件foo了。当echo想向stdout文件写入“hello!”时自然就写入到了foo中。
        (6) 回到shell后,关闭指向foo的1号与3号文件文件,再用dup()和close()将2号恢复至stdout,这样shell就恢复了0、1、2三个标准输入/输出文件。

        由此可见,当echo程序(或其他)在运行的时候并不知道stdout(对于stdin和stderr同样)指向什么,进程与实际输出文件或设备的结合是在运行时由其父进程“包办”的。这样就简化了子进程的程序设计,因为在设计时只要跟三个逻辑上存在的文件打交道就可以了。可能有人会觉得这很像面向对象中 的多态和重载,没有什么新奇之处,但是如果你活在30甚至40年前,可能你会改变你的看法。 

    参考2:http://www.cnblogs.com/GODYCA/archive/2013/01/05/2846197.html

    由于利用管道实现进程间通信,是通过创建两个文件描述符,但是描述符的初始化是通过随机的,就是从可用的文件描述符中取出,并将可用的文件描述符与 file对象相关联,如果我们需要将管道的两头与其他的流相关时,就需要重定向操作,重定向fd[0]和fd[1]的file,下面是关于实现重定向的函 数dup和dup2的解释:

    系统调用dup和dup2能够复制文件描述符。dup返回新的文件文件描述符(没有用的文件描述符最小的编号)。dup2可以让用户指定返回的文件描述符的值,如果需要,则首先接近newfd的值,他通常用来重新打开或者重定向一个文件描述符。

    他的原型如下:

    #include <unsitd.h>

    int dup(int oldfd);

    int dup2(int oldfd,int newfd);

    dup 和dup2都是返回新的描述符。或者返回-1并设置 errno变量。新老描述符共享文件的偏移量(位置)、标志和锁,但是不共享close-on-exec标志。

    相信大部分在Unix/Linux下编程的程序员手头上都有《Unix环境高级编程》(APUE)这本超级经典巨著。作者在该书中讲解dup/dup2之前曾经讲过“文件共享”,这对理解dup/dup2还是很有帮助的。这里做简单摘录以备在后面的分析中使用:
    Stevens said:
    (1) 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
       (a) 文件描述符标志。
       (b) 指向一个文件表项的指针。
    (2) 内核为所有打开文件维持一张文件表。每个文件表项包含:
       (a) 文件状态标志(读、写、增写、同步、非阻塞等)。
       (b) 当前文件位移量。
       (c) 指向该文件v节点表项的指针。
    图示:
       文件描述符表
       ------------
    fd0 0   | p0 -------------> 文件表0 ---------> vnode0
       ------------
    fd1 1   | p1 -------------> 文件表1 ---------> vnode1
       ------------
    fd2 2   | p2
       ------------
    fd3 3   | p3
       ------------
    ... ...
    ... ...
       ------------

    一、单个进程内的dup和dup2
    假设进程A拥有一个已打开的文件描述符fd3,它的状态如下

    进程A的文件描述符表(before dup2)
       ------------
    fd0 0   | p0
       ------------
    fd1 1   | p1 -------------> 文件表1 ---------> vnode1
       ------------
    fd2 2   | p2
       ------------
    fd3 3   | p3 -------------> 文件表2 ---------> vnode2
       ------------
    ... ...
    ... ...
       ------------

    经下面调用:
    n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:

    进程A的文件描述符表(after dup2)
       ------------
    fd0 0   | p0
       ------------
    n_fd 1   | p1 ------------
       ------------               
    fd2 2   | p2                  
       ------------                 _|
    fd3 3   | p3 -------------> 文件表2 ---------> vnode2
       ------------
    ... ...
    ... ...
       ------------
    解释如下:
    n_fd = dup2(fd3, STDOUT_FILENO)表示n_fd与fd3共享一个文件表项(它们的文件表指针指向同一个文件表项),n_fd在文件描述符表中的位置为 STDOUT_FILENO的位置,而原先的STDOUT_FILENO所指向的文件表项被关闭,我觉得上图应该很清晰的反映出这点。按照上面的解释我们 就可以解释CU中提出的一些问题:
    (1) "dup2的第一个参数是不是必须为已打开的合法filedes?" -- 答案:必须。
    (2) "dup2的第二个参数可以是任意合法范围的filedes值么?" -- 答案:可以,在Unix其取值区间为[0,255]。

    另外感觉理解dup2的一个好方法就是把fd看成一个结构体类型,就如上面图形中画的那样,我们不妨把之定义为:
    struct fd_t {
    int index;
    filelistitem *ptr;
    };
    然后dup2匹配index,修改ptr,完成dup2操作。

    在学习dup2时总是碰到“重定向”一词,上图完成的就是一个“从标准输出到文件的重定向”,经过dup2后进程A的任何目标为STDOUT_FILENO的I/O操作如printf等,其数据都将流入fd3所对应的文件中。下面是一个例子程序:
    #define TESTSTR "Hello dup2 "
    int main() {
            int     fd3;

            fd3 = open("testdup2.dat", 0666);
            if (fd < 0) {
                    printf("open error ");
                    exit(-1);
            }

            if (dup2(fd3, STDOUT_FILENO) < 0) {       
                    printf("err in dup2 ");
            }
            printf(TESTSTR);
            return 0;
    }
    其结果就是你在testdup2.dat中看到"Hello dup2"。

    二、重定向后恢复
    CU上有这样一个帖子,就是如何在重定向后再恢复原来的状态?首先大家都能想到要保存重定向前的文件描述符。那么如何来保存呢,象下面这样行么?
    int s_fd = STDOUT_FILENO;
    int n_fd = dup2(fd3, STDOUT_FILENO);
    还是这样可以呢?
    int s_fd = dup(STDOUT_FILENO);
    int n_fd = dup2(fd3, STDOUT_FILENO);
    这 两种方法的区别到底在哪呢?答案是第二种方案才是正确的,分析如下:按照第一种方法,我们仅仅在"表面上"保存了相当于fd_t(按照我前面说的理解方 法)中的index,而在调用dup2之后,ptr所指向的文件表项由于计数值已为零而被关闭了,我们如果再调用dup2(s_fd, fd3)就会出错 (出错原因上面有解释)。而第二种方法我们首先做一下复制,复制后的状态如下图所示:
    进程A的文件描述符表(after dup)
       ------------
    fd0 0   | p0
       ------------
    fd1 1   | p1 -------------> 文件表1 ---------> vnode1
       ------------                 /|
    fd2 2   | p2                /
       ------------             /
    fd3 3   | p3 -------------> 文件表2 ---------> vnode2
       ------------          /
    s_fd 4   | p4 ------/
       ------------
    ... ...
    ... ...
       ------------

    调用dup2后状态为:
    进程A的文件描述符表(after dup2)
       ------------
    fd0 0   | p0
       ------------
    n_fd 1   | p1 ------------
       ------------               
    fd2 2   | p2                 
       ------------                _|
    fd3 3   | p3 -------------> 文件表2 ---------> vnode2
       ------------
    s_fd 4   | p4 ------------->文件表1 ---------> vnode1
       ------------
    ... ...
    ... ...
       ------------
    dup(fd)的语意是返回的新的文件描述符与fd共享一个文件表项。就如after dup图中的s_fd和fd1共享文件表1一样。

    确定第二个方案后重定向后的恢复就很容易了,只需调用dup2(s_fd, n_fd);即可。下面是一个完整的例子程序:
    #define TESTSTR "Hello dup2 "
    #define SIZEOFTESTSTR 11

    int main() {
            int     fd3;
            int     s_fd;
            int     n_fd;

            fd3 = open("testdup2.dat", 0666);
            if (fd3 < 0) {
                    printf("open error ");
                    exit(-1);
            }

           
            s_fd = dup(STDOUT_FILENO);
            if (s_fd < 0) {
                    printf("err in dup ");
            }

           
            n_fd = dup2(fd3, STDOUT_FILENO);
            if (n_fd < 0) {
                    printf("err in dup2 ");
            }
            write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR);  

           
            if (dup2(s_fd, n_fd) < 0) {
                    printf("err in dup2 ");
            }
            write(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR);
            return 0;
    }
    注 意这里我在输出数据的时候我是用了不带缓冲的write库函数,如果使用带缓冲区的printf,则最终结果为屏幕上输出两行"Hello dup2",而文件testdup2.dat中为空,原因就是缓冲区作怪,由于最终的目标是屏幕,所以程序最后将缓冲区的内容都输出到屏幕。


    三、父子进程间的dup/dup2
    由fork调用得到的子进程和父进程的相同文件描述符共享同一文件表项,如下图所示:
    父进程A的文件描述符表
       ------------
    fd0 0   | p0
       ------------
    fd1 1   | p1 -------------> 文件表1 ---------> vnode1
       ------------                            /|
    fd2 2   | p2                             |
       ------------                            |
                                                   |
    子进程B的文件描述符表                |
       ------------                             |
    fd0 0   | p0                             |
       ------------                             |
    fd1 1   | p1 ---------------------|
       ------------
    fd2 2   | p2
       ------------
    所以恰当的利用dup2和dup可以在父子进程之间建立一条“沟通的桥梁”。这里不详述。

    四、小结
    灵活的利用dup/dup2可以给你带来很多强大的功能,花了一些时间总结出上面那么多,不知道自己理解的是否透彻,只能在以后的实践中慢慢探索了。

    参考3:

    dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。
    它们经常用来重定向进程的stdin、stdout和stderr。
    这两个函数的 原形如下:
    #include <unistd.h>
    int dup( int oldfd );
    int dup2( int oldfd, int targetfd )

    利用函数dup,我们可以复制一个描述符。传给该函数一个既有的描述符,它就会返回一个新的描述符,
    这个新的描述符是传给它的描述符的拷贝。这意味着,这两个描述符共享同一个数据结构。例如,
    如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。
    下面是用来说明dup函数使用方法的代码片段:

    int fd1, fd2;
        ...
    fd2 = dup( fd1 );


    需要注意的是,我们可以在调用fork之前建立一个描述符,这与调用dup建立描述符的效果是一样的,
    子进程也同样会收到一个复制出来的描述符。

    dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,
    目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,
    两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。下面我们用一段代码加以说明:      
    int oldfd;
    oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
    dup2( oldfd, 1 );
    close( oldfd );

    本例中,我们打开了一个新文件,称为“app_log”,并收到一个文件描述符,该描述符叫做fd1。我们调用dup2函数,
    参数为oldfd和1,这会导致用我们新打开的文件描述符替换掉由1代表的文件描述符(即stdout,因为标准输出文件的id为1)。
    任何写到stdout的东西,现在都将改为写入名为“app_log”的文件中。

    需要注意的是,dup2函数在复制了oldfd之后,会立即将其关闭,但不会关掉新近打开的文件描述符,因为文件描述符1现在也指向它。

    下面我们介绍一个更加深入的示例代码。回忆一下本文前面讲的命令行管道,在那里,我们将ls –1命令的标准输出作为标准输入
    连接到wc –l命令。接下来,我们就用一个C程序来加以说明这个过程的实现。代码如下面的示例代码3所示。
    在示例代码3中,首先在第9行代码中建立一个管道,然后将应用程序分成两个进程:一个子进程(第13–16行)
    和一个父进程(第20–23行)。接下来,在子进程中首先关闭stdout描述符(第13行),然后提供了ls –1命令功能,
    不过它不是写到stdout(第13行),而是写到我们建立的管道的输入端,这是通过dup函数来完成重定向的。在第14行,
    使用dup2 函数把stdout重定向到管道(pfds[1])。之后,马上关掉管道的输入端。然后,使用execlp函数把子进程的
    映像替换为命令ls –1的进程映像,一旦该命令执行,它的任何输出都将发给管道的输入端。

    现在来研究一下管道的接收端。从代码中可以看出,管道的接收端是由父进程来担当的。首先关闭stdin描述符(第20行),
    因为我们不会从机器的键盘等标准设备文件来接收数据的输入,而是从其它程序的输出中接收数据。然后,再一次用到dup2函数(第21行),
    让stdin变成管道的输出端,这是通过让文件描述符0(即常规的stdin)等于pfds[0]来实现的。关闭管道的stdout端(pfds[1]),
    因为在这里用不到它。最后,使用 execlp函数把父进程的映像替换为命令wc -1的进程映像,命令wc -1把管道的内容作为它的输入(第23行)。

    示例代码3:利用C实现命令的流水线操作的代码

         1:       #include <stdio.h>
         2:       #include <stdlib.h>
         3:       #include <unistd.h>
         4:
         5:       int main()
         6:       ...{
         7:         int pfds[2];
         8:
         9:         if ( pipe(pfds) == 0 ) ...{   //建立一个管道
         10:
         11:           if ( fork() == 0 ) ...{   //子进程
         12:
         13:             close(1);     //关闭stdout描述符
         14:             dup2( pfds[1], 1 );   //把stdout重定向到管道(pfds[1])
         15:             close( pfds[0] );    //关掉管道的输入端
         16:             execlp( "ls", "ls", "-1", NULL ); //把子进程的映像替换为命令ls –1的进程映像
         17:
         18:           } else ...{     //父进程

    19:
         20:             close(0);     //关闭stdin描述符
         21:             dup2( pfds[0], 0 );   //让stdin变成管道的输出端
         22:             close( pfds[1] );    //关闭管道的stdout端(pfds[1])
         23:             execlp( "wc", "wc", "-l", NULL ); //把父进程的映像替换为命令wc -1的进程映像
         24:
         25:           }
         26:
         27:         }
         28:
         29:         return 0;
         30:       }


    在该程序中,需要格外关注的是,我们的子进程把它的输出重定向的管道的输入,然后,父进程将它的输入重定向到管道的输出。
    这在实际的应用程序开发中是非常有用的一种技术。

    1. 文件描述符在内核中数据结构
        在具体说dup/dup2之前, 我认为有必要先了解一下文件描述符在内核中的形态。
    一个进程在此存在期间,会有一些文件被打开,从而会返回一些文件描述符,从shell
    中运行一个进程,默认会有3个文件描述符存在(0、1、2), 0与进程的标准输入相关联,
    1与进程的标准输出相关联,2与进程的标准错误输出相关联,一个进程当前有哪些打开
    的文件描述符可以通过/proc/进程ID/fd目录查看。 下图可以清楚的说明问题:

      进程表项

    ————————————————
       fd标志 文件指针
           _____________________

    fd 0:|________|____________|------------> 文件表

    fd 1:|________|____________|

    fd 2:|________|____________|

    fd 3:|________|____________|


          |     .......         |

          |_____________________|

                    图1

    文件表中包含:文件状态标志、当前文件偏移量、v节点指针,这些不是本文讨论的
    重点,我们只需要知道每个打开的文件描述符(fd标志)在进程表中都有自己的文件表
    项,由文件指针指向。

    2. dup/dup2函数
    APUE和man文档都用一句话简明的说出了这两个函数的作用:复制一个现存的文件描述符。
    #include <unistd.h>
    int dup(int oldfd);
    int dup2(int oldfd, int newfd);

    从图1来分析这个过程,当调用dup函数时,内核在进程中创建一个新的文件描述符,此
    描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。
      进程表项

    ————————————————

       fd标志 文件指针

           _____________________

    fd 0:|________|____________|                   ______

    fd 1:|________|____________|----------------> |      |

    fd 2:|________|____________|                  |文件表|

    fd 3:|________|____________|----------------> |______|

          |     .......         |
          |_____________________|



                    图2:调用dup后的示意图
    如图2 所示,假如oldfd的值为1, 当前文件描述符的最小值为3, 那么新描述符3指向
    描述符1所拥有的文件表项。

    dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则
    先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。dup2函数返回的新
    文件描述符同样与参数oldfd共享同一文件表项。

    APUE用另外一个种方法说明了这个问题:
    实际上,调用dup(oldfd);
    等效与

            fcntl(oldfd, F_DUPFD, 0)
    而调用dup2(oldfd, newfd);
    等效与

            close(oldfd);
            fcntl(oldfd, F_DUPFD, newfd);

    3. CGI中dup2
    写过CGI程序的人都清楚,当浏览器使用post方法提交表单数据时,CGI读数据是从标准
    输入stdin, 写数据是写到标准输出stdout(c语言利用printf函数)。按照我们正常的理
    解,printf的输出应该在终端显示,原来CGI程序使用dup2函数将STDOUT_FINLENO(这个
    宏在unitstd.h定义,为1)这个文件描述符重定向到了连接套接字。

    dup2(connfd, STDOUT_FILENO); /*实际情况还涉及到了管道,不是本文的重点*/
    如第一节所说, 一个进程默认的文件描述符1(STDOUT_FILENO)是和标准输出stdout相
    关联的,对于内核而言,所有打开的文件都通过文件描述符引用,而内核并不知道流的
    存在(比如stdin、stdout),所以printf函数输出到stdout的数据最后都写到了文件描述
    符1里面。至于文件描述符0、1、2与标准输入、标准输出、标准错误输出相关联,这
    只是shell以及很多应用程序的惯例,而与内核无关。
    用下面的流图可以说明问题:(ps: 虽然不是流图关系,但是还是有助于理解)
    printf -> stdout -> STDOUT_FILENO(1) -> 终端(tty)
    printf最后的输出到了终端设备,文件描述符1指向当前的终端可以这么理解:
    STDOUT_FILENO = open("/dev/tty", O_RDWR);

    使用dup2之后STDOUT_FILENO不再指向终端设备, 而是指向connfd, 所以printf的
    输出最后写到了connfd。是不是很优美?:)


    4. 如何在CGI程序的fork子进程中还原STDOUT_FILENO
    如果你能看到这里,感谢你的耐心, 我知道很多人可能感觉有点复杂, 其实
    复杂的问题就是一个个小问题的集合。所以弄清楚每个小问题就OK了,第三节中
    说道,STDOUT_FILENO被重定向到了connfd套接字, 有时候我们可能想在CGI程序
    中调用后台脚本执行,而这些脚本中难免会有一些输入输出, 我们知道fork之后,
    子进程继承了父进程的所有文件描述符,所以这些脚本的输入输出并不会如我们愿
    输出到终端设备,而是和connfd想关联了,这个显然会扰乱网页的输出。那么如何
    恢复STDOUT_FILENO和终端关联呢?

    方法1:在dup2之前保存原有的文件描述符,然后恢复。
    代码实现如下:
    savefd = dup(STDOUT_FILENO); /*savefd此时指向终端*/
    dup2(connfd, STDOUT_FILENO);   /*STDOUT_FILENO(1) 被重新指向connfd*/
    ..... /*处理一些事情*/
    dup2(savefd, STDOUT_FILENO); /*STDOUT_FILENO(1) 恢复指向savefd*/

    很遗憾CGI程序无法使用这种方法, 因为dup2这些不是在CGI程序中完成的,而是在
    web server中实现的,修改web server并不是个好主意。

    方法2: 追本溯源,打开当前终端恢复STDOUT_FILENO。
    分析第三节的流图, STDOUT_FILENO是如何和终端关联的? 我们重头做一遍不就行
    了, 代码实现如下:

    ttyfd = open("/dev/tty", O_RDWR);
    dup2(ttyfd, STDOUT_FILENO);
    close(ttyfd);
    /dev/tty是程序运行所在的终端, 这个应该通过一种方法获得。实践证明这种方法
    是可行的,但是我总感觉有些不妥,不知道为什么,可能一些潜在的问题还没出现。

    可以考虑一下下面的程序:

    #include <stdio.h>
     2 #include <unistd.h>
     3 #include <sys/types.h>
     4 #include <sys/stat.h>
     5 #include <fcntl.h>
     6 #include <string.h>
     7 
     8 
     9 int main(int argc, const char *argv[])
    10 {
    11 
    12 
    13     int fd1 = open("test.txt", O_RDWR);
    14 
    15     int fd2 = dup(fd1);
    16     int fd3;
    17 
    18     char *buf = "石家庄铁道大学
    ";
    19 
    20     printf("pengdognlin137
    ");
    21 
    22     write(fd1,buf, strlen(buf));
    23     write(fd2,buf, strlen(buf));
    24 
    25     sleep(3);                                                                                                               
    26     close(1);
    27     fd3 = dup(fd1);
    28     printf("pengdognlin137@163.com
    ");
    29 
    30     close(fd1);
    31     close(fd2);
    32 
    33     return 0;
    34 }
  • 相关阅读:
    图解HTTPS
    JQuery 控件
    sql server 中某个字段值合并【转】
    ASP.NET时间函数及其格式转换
    数据库 'tempdb' 的日志已满
    @@ERROR 和 @@ROWCOUNT
    SQL Server中行列转换 Pivot UnPivot 【转】
    Global.asax详解
    SQL Server 2008时提示评估期已过的解决办法
    C# IO读取文件问题:正由另一进程使用
  • 原文地址:https://www.cnblogs.com/pengdonglin137/p/3286627.html
Copyright © 2011-2022 走看看