中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

JDK 源碼閱讀 : FileDescriptor

2018-07-02    來源:importnew

容器云強(qiáng)勢上線!快速搭建集群,上萬Linux鏡像隨意使用

操作系統(tǒng)使用文件描述符來指代一個(gè)打開的文件,對文件的讀寫操作,都需要文件描述符作為參數(shù)。Java雖然在設(shè)計(jì)上使用了抽象程度更高的流來作為文件操作的模型,但是底層依然要使用文件描述符與操作系統(tǒng)交互,而Java世界里文件描述符的對應(yīng)類就是FileDescriptor。

Java文件操作的三個(gè)類:FileIntputStream,FileOutputStream,RandomAccessFile,打開這些類的源碼可以看到都有一個(gè)FileDescriptor成員變量。

注:本文使用的JDK版本為8。

FileDescriptor與文件描述符

操作系統(tǒng)中的文件描述符本質(zhì)上是一個(gè)非負(fù)整數(shù),其中0,1,2固定為標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出,程序接下來打開的文件使用當(dāng)前進(jìn)程中最小的可用的文件描述符號碼,比如3。

文件描述符本身就是一個(gè)整數(shù),所以FileDescriptor的核心職責(zé)就是保存這個(gè)數(shù)字:

public final class FileDescriptor {
    private int fd;
}

但是文件描述符是無法在Java代碼里設(shè)置的,因?yàn)镕ileDescriptor只有私有和無參的構(gòu)造函數(shù):

public FileDescriptor() {
    fd = -1;
}
private FileDescriptor(int fd) {
    this.fd = fd;
}

那Java是在何時(shí)會設(shè)置FileDescriptor的fd字段呢?這要結(jié)合FileIntputStreamFileOutputStream,RandomAccessFile的代碼來看了。

我們以FileInputStream為例,首先,FileInputStream有一個(gè)FileDescriptor成員變量:

public class FileInputStream extends InputStream
{
    private final FileDescriptor fd;

FileInputStream實(shí)例化時(shí),會新建FileDescriptor實(shí)例,并使用fd.attach(this)關(guān)聯(lián)FileInputStream實(shí)例與FileDescriptor實(shí)例,這是為了日后關(guān)閉文件描述符做準(zhǔn)備。

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name);
}
private void open(String name) throws FileNotFoundException {
    open0(name);
}
private native void open0(String name) throws FileNotFoundException;

但是上面的代碼也沒有對FileDescriptor#fd進(jìn)行賦值,實(shí)際上Java層面無法對他賦值,真正的邏輯是在FileInputStream#open0這個(gè)native方法中,這就要下載JDK的源碼來看了:

// /jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
}
// /jdk/src/solaris/native/java/io/io_util_md.c
void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;
#if defined(__linux__) || defined(_ALLBSD_SOURCE)
        /* Remove trailing slashes, since the kernel won't */
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
#endif
        fd = JVM_Open(ps, flags, 0666); // 打開文件拿到文件描述符
        if (fd >= 0) {
            SET_FD(this, fd, fid); // 非負(fù)整數(shù)認(rèn)為是正確的文件描述符,設(shè)置到fd字段
        } else {
            throwFileNotFoundException(env, path);  // 負(fù)數(shù)認(rèn)為是不正確文件描述符,拋出FileNotFoundException異常
        }
    } END_PLATFORM_STRING(env, ps);
}

可以看到JDK的JNI代碼中,使用JVM_Open打開文件,得到文件描述符,而JVM_Open已經(jīng)不是JDK的方法了,而是JVM提供的方法,所以我們需要在hotspot中尋找其實(shí)現(xiàn):

// /hotspot/src/share/vm/prims/jvm.cpp
JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode))
  JVMWrapper2("JVM_Open (%s)", fname);
  //%note jvm_r6
  int result = os::open(fname, flags, mode);  // 調(diào)用os::open打開文件
  if (result >= 0) {
    return result;
  } else {
    switch(errno) {
      case EEXIST:
        return JVM_EEXIST;
      default:
        return -1;
    }
  }
JVM_END
// /hotspot/src/os/linux/vm/os_linux.cpp
int os::open(const char *path, int oflag, int mode) {
  if (strlen(path) > MAX_PATH - 1) {
    errno = ENAMETOOLONG;
    return -1;
  }
  int fd;
  int o_delete = (oflag & O_DELETE);
  oflag = oflag & ~O_DELETE;
  fd = ::open64(path, oflag, mode);  // 調(diào)用open64打開文件
  if (fd == -1) return -1;
  // 問打開成功也可能是目錄,這里還需要判斷是否打開的是普通文件
  {
    struct stat64 buf64;
    int ret = ::fstat64(fd, &buf64);
    int st_mode = buf64.st_mode;
    if (ret != -1) {
      if ((st_mode & S_IFMT) == S_IFDIR) {
        errno = EISDIR;
        ::close(fd);
        return -1;
      }
    } else {
      ::close(fd);
      return -1;
    }
  }
#ifdef FD_CLOEXEC
    {
        int flags = ::fcntl(fd, F_GETFD);
        if (flags != -1)
            ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
    }
#endif
  if (o_delete != 0) {
    ::unlink(path);
  }
  return fd;
}

可以看到JVM最后使用open64這個(gè)方法打開文件,網(wǎng)上對于open64這個(gè)資料還是很少的,我找到的是man page for open64 (all section 2) – Unix & Linux Commands,從中可以看出,open64是為了在32位環(huán)境打開大文件的系統(tǒng)調(diào)用,但是不是標(biāo)志的一部分。

這里的open不是我們以前學(xué)C語言時(shí)打開文件用的fopen函數(shù),fopen是C標(biāo)準(zhǔn)庫里的函數(shù),而open不是,open是POSIX規(guī)范中的函數(shù),是不帶緩沖的I/O,不帶緩沖的I/O相關(guān)的函數(shù)還有read,write,lseek,close,不帶緩沖指的是這些函數(shù)都調(diào)用內(nèi)核中的一個(gè)系統(tǒng)調(diào)用,而C標(biāo)準(zhǔn)庫為了減少系統(tǒng)調(diào)用,使用了緩存來減少read,write的內(nèi)存調(diào)用。(參考《UNIX環(huán)境高級編程》)

通過上面的代碼跟蹤,我們知道了FileInputStream#open是使用open系統(tǒng)調(diào)用來打開文件,得到文件句柄,現(xiàn)在我們的問題要回到這個(gè)文件句柄是如何最終設(shè)置到FileDescriptor#fd,我們來看/jdk/src/solaris/native/java/io/io_util_md.c:fileOpen的關(guān)鍵代碼:

fd = handleOpen(ps, flags, 0666);
if (fd != -1) {
    SET_FD(this, fd, fid);
} else {
    throwFileNotFoundException(env, path);
}

如果文件描述符fd正確,通過SET_FD這個(gè)紅設(shè)置到fid對應(yīng)的成員變量上:

#define SET_FD(this, fd, fid) \
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))

SET_FD宏比較簡單,獲取FileInputStream上的fid這個(gè)字段ID對應(yīng)的字段,然后設(shè)置這個(gè)字段的IO_fd_fdID對應(yīng)的字段(FileDescriptor#fd)為文件描述符。

那這個(gè)fidIO_fd_fdID是哪里來的呢?在/jdk/src/share/native/java/io/FileInputStream.c的開頭,可以看到這樣的代碼:

jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */
/**************************************************************
 * static methods to store field ID's in initializers
 */
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_initIDs(JNIEnv *env, jclass fdClass) {
    fis_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
}

Java_java_io_FileInputStream_initIDs對應(yīng)FileInputStream中static塊調(diào)用的initIDs函數(shù):

public class FileInputStream extends InputStream
{
    /* File Descriptor - handle to the open file */
    private final FileDescriptor fd;
    static {
        initIDs();
    }
    private static native void initIDs();
    // ...
}

還有jdk/src/solaris/native/java/io/FileDescriptor_md.c開頭:

/* field id for jint 'fd' in java.io.FileDescriptor */
jfieldID IO_fd_fdID;
/**************************************************************
 * static methods to store field ID's in initializers
 */
JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I");
}

Java_java_io_FileDescriptor_initIDs對應(yīng)FileDescriptor中static塊調(diào)用的initIDs函數(shù):

public final class FileDescriptor {
    private int fd;
    static {
        initIDs();
    }
    /* This routine initializes JNI field offsets for the class */
    private static native void initIDs();
}

從代碼可以看出這樣的一個(gè)流程:

  1. JVM加載FileDescriptor類,執(zhí)行static塊中的代碼
  2. 執(zhí)行static塊中的代碼時(shí),執(zhí)行initIDs本地方法
  3. initIDs本地方法只做了一件事情,就是獲取fd字段ID,并保存在IO_fd_fdID變量中
  4. JVM加載FileInputStream類,執(zhí)行static塊中的代碼
  5. 執(zhí)行static塊中的代碼時(shí),執(zhí)行initIDs本地方法
  6. initIDs本地方法只做了一件事情,就是獲取fd字段ID,并保存在fis_fd變量中
  7. 后續(xù)邏輯直接使用IO_fd_fdID和fis_fd

為什么會有這樣一個(gè)奇怪的初始化過程呢,為什么要專門弄一個(gè)initIDs方法來提前保存字段ID呢?這是因?yàn)樘囟惖淖侄蜪D在一次Java程序的聲明周期中是不會變化的,而獲取字段ID本身是一個(gè)比較耗時(shí)的過程,因?yàn)槿绻侄问菑母割惱^承而來,JVM需要遍歷繼承樹來找到這個(gè)字段,所以JNI代碼的最佳實(shí)踐就是對使用到的字段ID做緩存。(參考使用 Java Native Interface 的最佳實(shí)踐)

標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出

標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出是所有操作系統(tǒng)都支持的,對于一個(gè)進(jìn)程來說,文件描述符0,1,2固定是標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出。

Java對標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤輸出的支持也是通過FileDescriptor實(shí)現(xiàn)的,FileDescriptor中定義了in,out,err這三個(gè)靜態(tài)變量:

public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);

 

我們常用的System.out等,就是基于這三個(gè)封裝的:

public final class System {
    public final static InputStream in = null;
    public final static PrintStream out = null;
    public final static PrintStream err = null;
    /**
    * Initialize the system class.  Called after thread initialization.
    */
    private static void initializeSystemClass() {
        FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
        FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
        FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
        setIn0(new BufferedInputStream(fdIn));
        setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
        setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
    }
    private static native void setIn0(InputStream in);
    private static native void setOut0(PrintStream out);
    private static native void setErr0(PrintStream err);
}

System作為一個(gè)特殊的類,類構(gòu)造時(shí)無法實(shí)例化in/out/err,構(gòu)造發(fā)生在initializeSystemClass被調(diào)用時(shí),但是in/out/err是被聲明為final的,如果聲明時(shí)和類構(gòu)造時(shí)沒有賦值,是會報(bào)錯的,所以System在實(shí)現(xiàn)時(shí),先設(shè)置為null,然后通過native方法來在運(yùn)行時(shí)修改(學(xué)到了不少奇技淫巧。。),通過setIn0/setOut0/setErr0的注釋也可以說明這一點(diǎn):

/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
JNIEXPORT void JNICALL
Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}
JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}
JNIEXPORT void JNICALL
Java_java_lang_System_setErr0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"err","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

FileDescriptor關(guān)閉邏輯

FileDescriptor的代碼不多,除了上面提到的fd成員變量,initIDs初始化構(gòu)造方法,in/out/err三個(gè)標(biāo)準(zhǔn)描述符,只剩下attachcloseAll這兩個(gè)方法,這兩個(gè)方法和文件描述符的關(guān)閉有關(guān)。

上文提到過,FileInputStream在實(shí)例化時(shí),會新建FileDescriptor并調(diào)用FileDescriptor#attach方法綁定文件流與文件描述符。

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name);
}

FileDescriptor#attach實(shí)現(xiàn)如下:

synchronized void attach(Closeable c) {
    if (parent == null) {
        // first caller gets to do this
        parent = c;
    } else if (otherParents == null) {
        otherParents = new ArrayList<>();
        otherParents.add(parent);
        otherParents.add(c);
    } else {
        otherParents.add(c);
    }
}

如果FileDescriptor只和一個(gè)FileInputStream/FileOutputStream/RandomAccessFile有關(guān)聯(lián),則只是簡單的保存到parent成員中,如果有多個(gè)FileInputStream/FileOutputStream/RandomAccessFile有關(guān)聯(lián),則所有關(guān)聯(lián)的Closeable都保存到otherParents這個(gè)ArrayList中。

這里其實(shí)有個(gè)細(xì)節(jié),就是parent變量其實(shí)只在這個(gè)函數(shù)有用到,所以上面的邏輯完全可以寫成無論FileDescriptor和幾個(gè)Closeable對象有關(guān)聯(lián),都直接保存到otherParents這個(gè)ArrayList即可,但是極大的概率,一個(gè)FileDescriptor只會和一個(gè)FileInputStream/FileOutputStream/RandomAccessFile有關(guān)聯(lián),只有用戶調(diào)用FileInputStream(FileDescriptor fdObj)這樣樣的構(gòu)造函數(shù)才會出現(xiàn)多個(gè)Closeable對象對應(yīng)一個(gè)FileDescriptor的情況,這里其實(shí)是做了優(yōu)化,在大概率的情況下不新建ArrayList,減少一個(gè)對象的創(chuàng)建開銷。

接著看看FileInputStream如何進(jìn)行關(guān)閉操作,如何關(guān)閉關(guān)聯(lián)的FileDescriptor

public void close() throws IOException {
    synchronized (closeLock) {
        if (closed) {
            return;
        }
        closed = true;
    }
    if (channel != null) {
        channel.close();
    }
    fd.closeAll(new Closeable() {
        public void close() throws IOException {
            close0();
        }
    });
}
private native void close0() throws IOException;

首先通過鎖保證關(guān)閉流程不會被并發(fā)調(diào)用,設(shè)置成員closedtrue,接著關(guān)閉關(guān)聯(lián)的Channel,這個(gè)以后分析NIO的時(shí)候再來說。接著就是關(guān)閉FileDescriptor了。

FileDescriptor沒有提供close方法,而是提供了一個(gè)closeAll方法:

synchronized void closeAll(Closeable releaser) throws IOException {
    if (!closed) {
        closed = true;
        IOException ioe = null;
        try (Closeable c = releaser) {
            if (otherParents != null) {
                for (Closeable referent : otherParents) {
                    try {
                        referent.close();
                    } catch(IOException x) {
                        if (ioe == null) {
                            ioe = x;
                        } else {
                            ioe.addSuppressed(x);
                        }
                    }
                }
            }
        } catch(IOException ex) {
            /*
             * If releaser close() throws IOException
             * add other exceptions as suppressed.
             */
            if (ioe != null)
                ex.addSuppressed(ioe);
            ioe = ex;
        } finally {
            if (ioe != null)
                throw ioe;
        }
    }
}

FileDescriptor的關(guān)閉流程有點(diǎn)繞,效果是會把關(guān)聯(lián)的Closeable對象(其實(shí)只可能是FileInputStream/FileOutputStream/RandomAccessFile,而這三個(gè)類的close方法實(shí)現(xiàn)是一模一樣的)通通都關(guān)閉掉(效果是這些對象的closed設(shè)置為true,關(guān)聯(lián)的Channel關(guān)閉,這樣這個(gè)對象就無法使用了),最后這些關(guān)聯(lián)的對象中,只會有一個(gè)對象的close0本地方法被調(diào)用,這個(gè)方法中調(diào)用系統(tǒng)調(diào)用close來真正關(guān)閉文件描述符:

// /jdk/src/solaris/native/java/io/FileInputStream_md.c
JNIEXPORT void JNICALL
Java_java_io_FileInputStream_close0(JNIEnv *env, jobject this) {
    fileClose(env, this, fis_fd);
}
// /jdk/src/solaris/native/java/io/io_util_md.c
void fileClose(JNIEnv *env, jobject this, jfieldID fid)
{
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        return;
    }
    /* Set the fd to -1 before closing it so that the timing window
     * of other threads using the wrong fd (closed but recycled fd,
     * that gets re-opened with some other filename) is reduced.
     * Practically the chance of its occurance is low, however, we are
     * taking extra precaution over here.
     */
    SET_FD(this, -1, fid);
    // 嘗試關(guān)閉0,1,2文件描述符,需要特殊的操作。首先這三個(gè)是不能關(guān)閉的,
    // 如果關(guān)閉的,后續(xù)打開的文件就會占用這三個(gè)描述符,
    // 所以合理的做法是把要關(guān)閉的描述符指向/dev/null,實(shí)現(xiàn)關(guān)閉的效果
    // 不過Java代碼中,正常是沒辦法關(guān)閉0,1,2文件描述符的
    if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
        int devnull = open("/dev/null", O_WRONLY);
        if (devnull < 0) {
            SET_FD(this, fd, fid); // restore fd
            JNU_ThrowIOExceptionWithLastError(env, "open /dev/null failed");
        } else {
            dup2(devnull, fd);
            close(devnull);
        }
    } else if (close(fd) == -1) { // 關(guān)閉非0,1,2的文件描述符只是調(diào)用close系統(tǒng)調(diào)用
        JNU_ThrowIOExceptionWithLastError(env, "close failed");
    }
}

在回頭來討論一個(gè)問題,就是為什么關(guān)閉一個(gè)FileInputStream/FileOutputStream/RandomAccessFile,就要把他關(guān)聯(lián)的文件描述符所關(guān)聯(lián)的所有FileInputStream/FileOutputStream/RandomAccessFile對象都關(guān)閉呢?

這個(gè)可以看看FileInputStream#close的JavaDoc:

Closes this file input stream and releases any system resources
associated with the stream.
If this stream has an associated channel then the channel is closed
as well.

也就是說FileInputStream#close是會吧輸入/出流對應(yīng)的系統(tǒng)資源關(guān)閉的,也就是輸入/出流對應(yīng)的文件描述符會被關(guān)閉,而如果這個(gè)文件描述符還關(guān)聯(lián)這其他輸入/出流,如果文件描述符都被關(guān)閉了,這些流自然也就不能用了,所以closeAll里把這些關(guān)聯(lián)的流通通都關(guān)閉掉,使其不再可用。

總結(jié)

  • FileDescriptor的作用是保存操作系統(tǒng)中的文件描述符
  • FileDescriptor實(shí)例會被FileInputStream/FileOutputStream/RandomAccessFile持有,這三個(gè)類在打開文件時(shí),在JNI代碼中使用open系統(tǒng)調(diào)用打開文件,得到文件描述符在JNI代碼中設(shè)置到FileDescriptorfd成員變量上
  • 關(guān)閉FileInputStream/FileOutputStream/RandomAccessFile時(shí),會關(guān)閉底層對應(yīng)的文件描述符,如果此文件描述符被多個(gè)FileInputStream/FileOutputStream/RandomAccessFile對象持有,則這些對象都會被關(guān)閉。關(guān)閉是文件底層是通過調(diào)用close系統(tǒng)調(diào)用實(shí)現(xiàn)的。

參考資料

  • 《UNIX環(huán)境高級編程》
  • 每天進(jìn)步一點(diǎn)點(diǎn)——Linux中的文件描述符與打開文件之間的關(guān)系 – CSDN博客
  • UNIX再學(xué)習(xí) – 文件描述符 – CSDN博客
  • Linux探秘之用戶態(tài)與內(nèi)核態(tài) – aCloudDeveloper – 博客園
  • 關(guān)于內(nèi)核態(tài)和用戶態(tài)切換開銷的測試 – fireworks – 博客園
  • 系統(tǒng)調(diào)用真正的效率瓶頸在哪里? – 知乎
  • 使用 Java Native Interface 的最佳實(shí)踐
  • java – Why closing an Input Stream closes the associated File Descriptor as well, even the File Descriptor is shared among multiple streams ? – Stack Overflow

標(biāo)簽: linux 代碼

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:linux 如何更改網(wǎng)卡 MAC 地址

下一篇:深入Spring Boot :怎樣排查 java.lang.ArrayStoreException