一、Let's Encrypt

Let's Encrypt SSL证书是一个免费的公益项目,由Mozilla、Cisco、Akamai、IdenTrust、EFF等组织人员发起,主要的目是为了推进网站从HTTP向HTTPS过度的进程,目前已经有越来越多的商家加入和赞助支持。

使用Let's Encrypt生成域名证书的前置条件:

  1. 拥有域名,能自主配置DNS记录。或者提供web服务器验证,需要在网站目录下放一个文件,推荐第一种方式。
  2. 获取证书的环境要能访问到DNS服务器,因为中途需要校验DNS解析。
  3. 拥有主机的超级权限,中途需要更新和安装组件。

二、申请流程

第一步:拉代码,代码开源于Github

> git clone https://github.com/letsencrypt/letsencrypt
Cloning into 'letsencrypt'...
remote: Enumerating objects: 29, done.
remote: Counting objects: 100% (29/29), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 61424 (delta 7), reused 4 (delta 2), pack-reused 61395
Receiving objects: 100% (61424/61424), 20.12 MiB | 5.01 MiB/s, done.
Resolving deltas: 100% (44625/44625), done.

完成后执行命令生成证书:

> ./certbot-auto certonly -d *.maqian.art --manual \
>     --preferred-challenges dns \
>     --server https://acme-v02.api.letsencrypt.org/directory

解释一下各个参数的含义:

  • certonly: 表示当前为安装模式
  • --manual: 表示手动安装插件,不要自动安装了
  • --preferred-challenges dns: 校验方式为dns验证
  • -d *.maqian.art: 要生成的域名列表,可以为多个,如果是多个分别以-d加上即可
  • --server: Let's Encrypt ACME v2版本使用的服务器

接下来的步骤一直接受即可,直到出现添加DNS记录为止:

Requesting to rerun ./certbot-auto with root privileges...
[sudo] password for ma: 
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): maqian@dyxmq.cn

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for sinfor.maqian.io

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.maqian.art with the following value:

zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

此时需要在DNS服务商处添加dns解析,记录类型为TXT,记录为_acme-challenge,值为zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU。当DNS记录设置好后,新开一个终端查询解析是否生效:

> nslookup -type=txt _acme-challenge.maqian.art
Server:        100.100.2.136
Address:    100.100.2.136#53

Non-authoritative answer:
_acme-challenge.maqian.art    text = "zw1MeEkmGZOqSqiySp9Ke8S5a9BXC3O4tYzlbjwU-CU"

Authoritative answers can be found from:

当查询到的记录和给定的都一致之后按下任意键执行下一步,如果DNS验证成功就会出现以下信息:

Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/maqian.art-0001/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/maqian.art-0001/privkey.pem
   Your cert will expire on 2019-03-22. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

此时就表示证书已经申请完成了,存放的路径为:/etc/letsencrypt/live/maqian.art-0001。

> sudo ls /etc/letsencrypt/live/maqian.art-0001/ -l
total 4
lrwxrwxrwx 1 root root  39 Dec 22 23:46 cert.pem -> ../../archive/maqian.art-0001/cert1.pem
lrwxrwxrwx 1 root root  40 Dec 22 23:46 chain.pem -> ../../archive/maqian.art-0001/chain1.pem
lrwxrwxrwx 1 root root  44 Dec 22 23:46 fullchain.pem -> ../../archive/maqian.art-0001/fullchain1.pem
lrwxrwxrwx 1 root root  42 Dec 22 23:46 privkey.pem -> ../../archive/maqian.art-0001/privkey1.pem
-rw-r--r-- 1 root root 692 Dec 22 23:46 README

三、安装到nginx

上面一共生成了四个文件,各自的用途为:

  • cert.pem: Apache服务器端证书
  • chain.pem: Apache根证书和中继证书
  • fullchain.pem: Nginx所需要ssl_certificate文件
  • privkey.pem: 安全证书KEY文件

部署到nginx只需要添加一下指令即可:

ssl on;
ssl_certificate /etc/letsencrypt/live/maqian.art-0001/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/maqian.art-0001/privkey.pem;

打开网站,点开左上角地址栏的https,查看证书:

一、概述

docker默认存在/var/lib/docker目录下,一般情况下这个目录都没有单独挂载,都是放在根目录下的,目录较小。

为了避免占用太多/var目录空间,并且方便管理,可以把存储目录放到其他的文件夹,例如/data/docker。

二、步骤

创建想要修改的目录,假设是/data/docker,首先创建文件夹并赋予权限。

> mkdir /data/docker
> chgrp -R docker /data/docker

停掉docker,修改docker的systemd服务文件,位于/usr/lib/systemd/system/docker.service,修改ExecStart一行:

> systemctl restart docker
> sed -i 's#ExecStart=/usr/bin/dockerd#ExecStart=/usr/bin/dockerd --graph /data/docker#g' /usr/lib/systemd/system/docker.service

重启docker:

> systemctl daemon-reload
> systemctl start docker 

验证是否修改成功:

> docker info | grep "Root"
Docker Root Dir: /data/docker
> ll /data/docker/
total 48
drwx------ 2 root root 4096 Dec 22 22:39 builder
drwx--x--x 3 root root 4096 Dec 22 22:39 containerd
drwx------ 2 root root 4096 Dec 22 22:39 containers
drwx------ 3 root root 4096 Dec 22 22:39 image
drwxr-x--- 3 root root 4096 Dec 22 22:39 network
drwx------ 3 root root 4096 Dec 22 22:39 overlay2
drwx------ 4 root root 4096 Dec 22 22:39 plugins
drwx------ 2 root root 4096 Dec 22 22:39 runtimes
drwx------ 2 root root 4096 Dec 22 22:39 swarm
drwx------ 2 root root 4096 Dec 22 22:39 tmp
drwx------ 2 root root 4096 Dec 22 22:39 trust
drwx------ 2 root root 4096 Dec 22 22:39 volumes

来源:力扣(LeetCode)

链接:113. 路径总和 II

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

说明:叶子节点是指没有子节点的节点。

示例:

给定如下二叉树,以及目标和 sum = 22:

          5
         / \
        4   8
       /   / \
      11  13  4
     /  \    / \
    7    2  5   1

返回:

[
   [5,4,11,2],
   [5,8,4,5]
]

二、题解

说明:

此题是112. 路径总和Ⅰ的升级版(题解参考:),相对于第一题来说,有以下两点不通:

  1. 题Ⅰ只需要判断存在即可,该题需要找到所有可能的结果。
  2. 找到所有结果的同时,该题还需要保存所有的路径。

算法:

递归+深搜遍历所有的树节点,每遍历一个节点除了计算sum是否满足条件以外,还要记录下该节点。当遇到满足条件的叶节点,把当前路径加到结果中去。需要利用到两个辅助的数组。

代码:

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        dfs(root, sum);
        return m_ans;
    }
private:
    void dfs(TreeNode *root, int sum) {
        if(root == NULL) {
            return;
        }

        // 节点添加到临时的数组里面去
        m_vec.push_back(root->val);
        sum -= root->val;

        if (root->left == NULL && root->right == NULL && sum == 0) {
            // 当前临时数组加入到结果数组中
            m_ans.push_back(m_vec);
        }

        // 遍历左节点
        if (root->left) {
            dfs(root->left, sum);
        }
        
        // 遍历右节点
        if (root->right) {
            dfs(root->right, sum);
        }
        
        // 最关键的一步:回溯状态
        // 函数入口处把当前节点添加到临时数组里面去了,这里退出的时候要删掉这一个节点
        // 避免当前节点还存在于临时数组中,影响后续的遍历结果
        m_vec.pop_back();
    }

    vector<vector<int>> m_ans; // 保存所有的结果
    vector<int> m_vec; // 临时保存路径的数组
};

复杂度分析:

  • 时间复杂度:访问每个节点一次,时间复杂度为O(N) ,其中N是节点个数。
  • 空间复杂度:需要用到两个数组,其中一个是保存结果的数组可不算入空间占用,另外一个保存路径的临时数组最坏情况下(树是一条线,即类似链表的时候)占用O(N) ,平均和最优情况下(树是平衡树)占用O(log(N))。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/maximum-subarray/

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入:[-2,1,-3,4,-1,2,1,-5,4]
  • 输出:6
  • 解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

分治法并不能算是最优解,但是可以锻炼对该算法的掌握情况。本题不考虑此算法。

二、题解

2.1 贪心算法

算法:

  1. 遍历数组,使用sum变量计算每一个节点时的最大和,用ans表示当前所有区间的最大子序和。
  2. 运行到第i个节点的时候,如果sum > 0,说明对当前节点有增益效果,sum继续加上当前节点。
  3. 如果sum <= 0,说明之前的序列对当前节点来说没有增益效果,之前的和可以舍弃。sum从当前节点开始重新计算。
  4. 然后比较ans和sum的大小,较大的赋值给sum。

图例:

图片来源于解答:画解算法:53. 最大子序和 , © 著作权归作者所有 。

以数组[-2, 3, -1, 1, -3]为例,初始化时设置sum为0,ans为第一个元素的值:

访问第一个元素-2,当前sum为0,更新sum为当前节点的值-2,sum和ans对比ans还是-2:

image2781361f9b1cc7d6.png

访问第二个元素3,因为sum = -2,如果加上当前节点,会使得连续和变成1(还不如不加,不加是3)。因此重新计算sum,设置sum为当前节点值3,继续往前计算:

imagefd03c8e73879b133.png

访问第三个元素-1,此时sum > 0,对-1有增益效果,sum加上-1等于2,ans不变:

image1f06514c3fa63f36.png

第四个元素:

image8742e9aae039ff80.png

到第五个元素时,sum = 3,加上-3之后等于0,ans依旧等于3:

imagedaf7eae811ef6e59.png

然后到此结束,最大连续和是3。

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int ans, sum, i;
        ans = nums.size() > 0 ? nums[0] : 0;

        for (i = 0, sum = 0; i < nums.size(); i++) {
            if (sum <= 0) {
                sum = nums[i];
            } else {
                sum += nums[i];
            }

            ans = max(ans, sum);
        }
        return ans;
    }
};

image.png

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2.2 动态规划

算法:

其实动态规划的思路和上面的贪心算法差不多,关键的结果是动态规划的状态方程:假设dp[i]是第i个节点时候的最大连续和,那怎么计算它的值呢?

其实还是和上面的贪心算法一样,只要dp[i - 1]加上当前节点的和大于当前节点,那么dp[i]就等于和值。否则dp[i]就应该设置为当前节点值,所以它的状态转移方程是:

dp[0] = nums[0];
dp[i] = max(dp[i - 1] + nums[i], nums[i])

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int i, sum;
        vector<int> dp(nums.size());

        if (nums.size() == 0) {
            return 0;
        }

        dp[0] = nums[0];
        sum = dp[0];

        for (i = 1; i < nums.size(); i++) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            sum = max(dp[i], sum);
        }

        return sum;
    }
};

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n),整个状态中只用到了dp[i]和dp[i - 1],优化成O(1)。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/diameter-of-binary-tree

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。

示例 :

给定二叉树

          1
         / \
        2   3
       / \     
      4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

注意:两结点之间的路径长度是以它们之间边的数目表示。

二、题解

本题最重要的一个点是要理清题意,求的是最大直径,是树内任意两点间的距离的最大值,不是求根节点到任意节点间距离的最大值(即深度)。

想法:

任意一条路径可以被写成两个箭头(不同方向),每个箭头代表一条从某些点向下遍历到孩子节点的路径。

假设我们知道对于每个节点最长箭头距离分别为L,R,那么最优路径经过L + R + 1个节点。

算法:

按照常用方法计算一个节点的深度:max(depth of node.left, depth of node.right) + 1。在计算的同时,经过这个节点的路径长度为1 + (depth of node.left) + (depth of node.right)。搜索每个节点并记录这些路径经过的点数最大值,期望长度是结果-1

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int diameterOfBinaryTree(TreeNode* root) {
        unsigned int diameter = 1;
        depth(root, diameter);
        return diameter - 1;
    }
private:
    int depth(TreeNode *node, unsigned int &diameter) {
        unsigned int lDepth, rDepth;
        if (node == NULL) {
            return 0;
        }
        lDepth = depth(node->left, diameter);
        rDepth = depth(node->right, diameter);

        diameter = max(diameter, lDepth + rDepth + 1);

        return max(lDepth, rDepth) + 1;
    }
};

复杂度分析:

  • 时间复杂度:O(N),每个节点只访问一次。
  • 空间复杂度:O(N),深度优先搜索的栈开销。

2020-03-10添加

为了完成leetcode每日1题打卡计划,使用golang重新提交了一次。

计算深度和直径的方式和上面略有不同,原理还是一样的:

func depth(root *TreeNode, diameter *int) int {
    if root == nil {
        return 0
    }

    // 左子节点的深度
    lDepth := depth(root.Left, diameter) + 1
    // 右子节点的深度
    rDepth := depth(root.Right, diameter) + 1

    // 当前节点的直径
    curDiameter := lDepth + rDepth - 1
    // 更新最大的直径
    if *diameter < curDiameter {
        *diameter = curDiameter
    }

    // 返回最大深度
    if lDepth < rDepth {
        return rDepth
    } else {
        return lDepth
    }
}

func diameterOfBinaryTree(root *TreeNode) int {
    var diameter int
    diameter = 0
    depth(root, &diameter)
    return diameter
}

nginx中只有if关键字,并不支持else语法,if的使用方法为:

if ($xxx = xxx) {
    xxx
}

和代码不同的是:if条件语句判断相等时只要一个等号,不能是==

虽然不支持else,但是可以使用以下的方法来模拟实现else:

server {
    server_name *.maqian.io;
    listen 80;
    
    location / {
        set $is_matched 0;
        if ($host = a.maqian.io) {
            proxy_pass http://127.0.0.1:1001/;
            set $is_matched 1;
        }
        
        if ($host = b.maqian.io) {
            proxy_pass http://127.0.0.1:1002/;
            set $is_matched 1;
        }
        # 没有匹配到,跳转到默认页面
        if ($is_matched = 0) {
            proxy_pass http://127.0.0.1;
        }
        
        # xxx
        # xxx
        # xxx
    }
}

setuid、setgid、以及黏着位

setuid的作用是以该命令拥有者的权限去执行,比如修改密码的passwd命令,执行passwd时会拥有root权限,不然就修改不了/etc/passwd文件了。

而setgid的意思是以命令所有组的权限去执行,它们的标志位是s,出现在x的地方,例如-rwsr-xr-x

手动添加这一位的方式:

> touch ls
> chmod u+s ls # UID权限设置
> ll ls
-rwSrw-r--. 1 ma ma 0 Nov  3 00:50 ls
> chmod g+s ls # GID权限设置
> ll ls
-rwSrwSr--. 1 ma ma 0 Nov  3 00:50 ls

对于以下程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    // 打印实际用户ID和有效用户ID
    printf("uid: %u euid: %d\n", getuid(), geteuid());
    return 0;
}

编译后加上UID权限,分别使用不同的身份运行:

> make setuid
gcc setuid.c -o debug/setuid
> ll debug/setuid 
-rwxrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid
> chmod u+s debug/setuid # 加上UID权限位
> ll debug/setuid 
-rwsrwxr-x. 1 ma ma 8552 Nov  3 02:24 debug/setuid 
> sudo ./debug/setuid 
[sudo] password for ma: 
uid: 0 euid: 1000

可以看到,不管是以当前用户还是root用户运行,实际用户id都是一样的,都是其所有者。

二、黏着位(sticky bit)

关于黏着位找了很多资料也没有找到明确的描述,网上众说纷纭也没有清晰的描述出它的作用,最后还是在《UNIX环境高级编程》中找到一些更明确的解释:

The S_ISVTX bit has an interesting history. Onversions of the UNIX System that predated demand paging, this bit was known as the sticky bit.If it was set for an executable program file, then the first time the program was executed, a copy of the program’s text was saved in the swap area when the process terminated. (The text portion of a program is the machine instructions.) The program would then load into memory morequickly the next time it was executed, because the swap area was handled as a contiguous file, as compared to the possibly random location of data blocks in a normal UNIX file system. The sticky bit was often set for common application programs, such as the text editor and the passes of the C compiler. Naturally,therewas a limit to the number of sticky files that could be contained in the swap area beforerunning out of swap space, but it was a useful technique. The name sticky came about because the text portion of the file stuck around in the swap area until the system was rebooted. Later versions of the UNIX System referred to this as the saved-text bit; hence the constant S_ISVTX.With today’s newer UNIX systems, most of which have a virtual memory system and a faster file system, the need for this technique has disappeared.

在早期的unix系统中,如果一个程序被设置了黏着位,那么当它第一次执行结束之后,程序的正文段会被写入到交换空间中,以此加快后续使用的加载速度。因为交换空间是顺序存放,而磁盘上是随机的。它通常被设置成经常被使用的公用程序例如文本编辑器、编译器等,会一直持续到系统重启。

在后续的unix系统都设计了新的更快速的文件系统,所以这种用法逐渐消失。而对于这一黏着位“现在的用途”的描述是:

On contemporary systems, the use of the sticky bit has been extended. The Single UNIX Specification allows the sticky bit to be set for a directory. If the bit is set for a directory, a file in the directory can be removed or renamed only if the user has write permission for the directory and meets one of the following criteria:

  1. Owns the file
  2. Owns the directory
  3. Is the superuser

The directories /tmp and /var/tmp are typical candidates for the sticky bit—they are directories in which any user can typically create files. The permissions for these two directories are often read, write, and execute for everyone (user, group, and other). But users should not be able to delete or rename files owned by others.

现在的系统里面,黏着位已经被扩展了,它被用于目录权限上,如果一个目录设置了这一位,这个目录下的文件就只能被满足以下条件的用户重命名或者删除:

  1. 所有者
  2. 当前目录苏有这
  3. 超级用户

目录/tmp和/var/tmp是设置粘住位的候选者—这两个目录是任何用户都可在其中创建文件的目录。这两个目录对任一用户 (用户、组和其他)的许可权通常都是读、写和执行。但是用户不应能删除或更名属于其他人的文件,为此在这两个目录的文件方式中都设置了粘住位

手动给目录添加sticky位:

> mkdir 123
> chmod +t 123
> ll -d 123
drwxrwxr-t. 2 ma ma 6 Nov  3 02:18 123

加上这一个权限之后目录的颜色也有变化:

image

linux c获取文件路径和文件名

linux提供了两个函数分别用来获取文件所在的目录和文件名:

char *dirname(char *path);
char *basename(char *path);

它们被包含在头文件libgen.h中,dirname取得的路径名不包含最后的/basename取自于最后一个下划线后的内容。

以下是几个示例:

路径dirnamebasename
/usr/lib/usrlib
/usr//usr
usr.usr
...
.....

通过这两个函数我们就可以写出属于我们自己的dirname和basename命令了:

#include <libgen.h>
#include <stdio.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Usage: %s path ...\n", argv[0]);
        return 0;
    }

    for (int i = 1; i < argc; i++) {
        printf("%s\n", dirname(argv[i])); // basename直接替换这里的函数即可
    }

    return 0;
}

编译运行:

> gcc dirname.c -o dirname
> ./dirname /usr/local /data/
/usr
/data

实际上linux也内置了dirnamebasename两个命令:

> basename /usr/lib/libDeployPkg.so.0 
libDeployPkg.so.0
> dirname /usr/local/ /data
/usr
/

一、原理

linux支持多进程间共享打开文件,即同一时刻允许多个进程同时打开同个文件,每个进程之间的读写操作互不影响。为了实现这一个机制,linux内核使用了三种数据结构来表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

1.1 内核数据结构

每个进程的进程表中有一个记录项,包含了当前进程所有打开的文件描述符,它包含了一个指向文件表项的指针和文件描述符标志。内核中,为所有打开的文件维持一张表,它包含了以下内容:

  • 当前文件打开的状态:以何种方式打开的该文件,只读、只写或是可读可写等。
  • 当前文件的偏移量:当前文件指针所处的位置。
  • 指向该文件节点表的指针:节点包含了当前文件的属性信息。

每个文件的信息被封装在一个v节点表项中,包含了当前文件的文件名、所有者以及inode等信息。

三者之间的状态关系为:

1.2 多进程共享同一个文件

对于多个进程打开的同一个文件,其状态关系为:

正因为每个文件描述符都有一个属于自己的文件表项,所以每个进程间的文件指针偏移相互独立,互相读写不干扰。但是打开同一个文件的时候v节点指针都指向同一个节点:

  • 每次完成write后,文件表项的当前文件指针偏移量也会立马加上写入的字节数。
  • 如果打开文件的时候加了O_APPEND参数,每次写入数据前会先把偏移量设置到文件末尾。
  • 通过lseek函数只修改当前文件偏移量,不进行任何I/O操作。

有一个要注意的是,每次fork进程后,子进程会复制父进程的文件描述符,两者相互独立。

二、dup和dup2

dup和dup2都可以用来复制一个现有的文件描述符,其用法如下:

#include <unistd.h>

int dup(int fd);
int dup2(int fd1, int fd2);

dup函数直接把复制后的文件描述符返回,返回的一定是当前文件描述符表中的最小数值。

对于dup2,可以通过fd2表示新描述符的值,如果fd2已经打开,系统会先关闭。如果fd1等于fd2,则直接返回不关闭。

复制过后的文件描述符共享一个文件表项,共享后的状态如下:

我们可以通过一个程序来验证这一个结论:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    char buff[6] = { 0 };
    int fd_1, fd_2;

    fd_1 = open("data.txt", O_RDONLY);
    if (fd_1 == -1) {
        perror("open file error");
        return -1;
    }

    fd_2 = dup(fd_1);
    if (fd_2 == -1) {
        perror("dup error");
        return -1;
    }

    if (read(fd_1, buff, 5) == -1) {
        perror("read error at fd_1");
        return -1;
    }
    printf("fd_1 read: %s\n", buff);

    if (read(fd_2, buff, 5) == -1) {
        perror("read error at fd_2");
    }
    printf("fd_2 read: %s\n", buff);

    close(fd_1);
    close(fd_2);

    return 0;
}

上面的代码中通过fd_1打开文件data.txtfd_2复制fd_1,两个文件描述符文件从文件中读取5个字节数据并打印出来。

我们编译代码执行:

# 先写十个字节数据到文件
> echo "HelloWorld" > data.txt
> mkdir debug
# 编译
> gcc dup.c -o debug/dup
# 执行
>  ./debug/dup 
fd_1 read: Hello
fd_2 read: World

可以看到,fd_2读取的数据是从第5个字节开始,即从fd_1读完偏移处开始,两者确实共享了同一个文件表项。