Linux PID namespace分析(2)

March 11th, 2015 by JasonLe's Tech 1,127 views

PID namespace 主要完成的功能是让不同的namespace中可以有相同的PID,子namespace与root namespace之前存在映射关系。

完成这个PID功能的主要由两个结构:

struct upid,主要表示某个特定namespace中的pid实例,而struct pid可以理解为在根root namespace中存在的pid实例,而这个实例在每个子namespace中可以有不同的upid,这个时候struct pid 下的numbers成员可以表示,在声明中我们看到numbers的大小只有1,我们可以理解为一个系统只有一个全局namespace。

如果我们创建一个子namespace,numbers的大小会动态分配,然后向namespace中添加更多的upid。
默认我们可以理解为全局namespace只有一层也就是level=0 numbers也只有1个实例。

 50 struct upid {
 51         /* Try to keep pid_chain in the same cacheline as nr for find_vpid */
 52         int nr;
 53         struct pid_namespace *ns;
 54         struct hlist_node pid_chain;
 55 };
 56
 57 struct pid
 58 {
 59         atomic_t count;
 60         unsigned int level;
 61         /* lists of tasks that use this pid */
 62         struct hlist_head tasks[PIDTYPE_MAX];
 63         struct rcu_head rcu;
 64         struct upid numbers[1];
 65 };

举例,比如我们在进行kill操作时,就会首先给进程发送一个signal,然后系统调用解析signal,最后调用到kill_something_info()的函数。http://lxr.free-electrons.com/source/kernel/signal.c#L1425

这个函数中有一个子函数kill_pid_info(),里面的pid_task()函数就可以看到可以通过pid与pid_type查找某个特定的task_struct。从而通过控制PCB块达到控制该进程的目的。

上面我们看到系统存在以下几种类型的PID,而这个在struct pid中声明了几个散列表头的节点,用来链接每个namespace中的upid。
我们可以看到PIDTYPE_MAX为3,通过这种方式我们可以动态添加pid_type,非常方便。这里的pid_type中没有线程组TGID,但是在struct task_struct 中存在groups_leader的成员,可以利用这个成员访问pid。

enum pid_type
{
         PIDTYPE_PID,
         PIDTYPE_PGID,
         PIDTYPE_SID,
         PIDTYPE_MAX
};

 

PID

通过看这个图,我们基本可以大致的弄清楚PID namespace的结构。

系统中可以有多个struct task_struct 结构,并且他们的pid相同,指向同一个pid实例,这就表明系统中不只存在一个全局命名空间,多个struct task_struct中的pid(我们需要注意一点是在struct task_struct中pid是存在于struct pid_link pids[PDTYPE_MAX]中,还有一个pid_t pid仅仅是数值而已。通过这种方式每个struct task_struct被组织到struct hlist_head tasks的散列表数组上。)指向同一个struct pid实例,这个就要分类来看到底是何种类型的pid,通过上面的枚举,可以看到可以存在PID、PGID、SID。

内核只关心全局的pid,因为该pid肯定是唯一的,子namespace的pid查看,只需查找相关映射即可。

上面说到的通过pid可以查找task_struct,原理就是查找查找struct pid中task散列表数组,然后通过对应特定类型的type,查找该散列表上面的特定元素。具体查看http://lxr.free-electrons.com/source/kernel/pid.c#L434

至于那个numbers的数据,存放的就是每个命名空间的实例,虽然这个数据类型是upid,但是upid内部有成员函数pid_namespace 。

pid_namespace 包括指向直接的父namespace成员。

 24 struct pid_namespace {
...
 30         struct task_struct *child_reaper;
 31         struct kmem_cache *pid_cachep;
 32         unsigned int level;
 33         struct pid_namespace *parent;
...
 48 };

通过这个upid,我们可以找到特定pid_namespace 。依照上图,level 0的命名空间就是全局命名空间,下面的level 1 2 都是子命名空间。这种层级的组织使得父namespace可以看到子namespace的PID,反之则不行。

具体实现:http://lxr.free-electrons.com/source/kernel/pid.c#L500

除了这种树形组织结构,这些upid实例还被组织到一个pid_hash的散列表数组中。这个变量是全局的。

static struct hlist_head *pid_hash;
575 void __init pidhash_init(void)
576 {
577         unsigned int i, pidhash_size;
578
579         pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
580                                            HASH_EARLY | HASH_SMALL,
581                                            &pidhash_shift, NULL,
582                                            0, 4096);
583         pidhash_size = 1U << pidhash_shift;
584
585         for (i = 0; i < pidhash_size; i++)
586                 INIT_HLIST_HEAD(&pid_hash[i]);
587 }

每个upid又会被组织到这个pid_hash,这样可以加快搜索。

最后就是通过各种参数查找pid、task_struct、namespace的实例,理解了数据结构,使用系统提供的内核API则不难。

 

 

参考:
http://blog.chinaunix.net/uid-20687780-id-1587537.html
http://blog.chinaunix.net/uid-20687780-id-1587608.html
http://blog.chinaunix.net/uid-20687780-id-1587702.html

Linux namespace分析(1)

March 9th, 2015 by JasonLe's Tech 1,329 views

在linux中实现kernel virtualization 的条件就是资源的限制与隔离,而namespace完成的系统资源的隔离。

从Linux 2.6.23开始 对于namespace的框架实现基本完成,之后的patch大多是修修补补,比较重要的一个patch是linux 3.8中一个非root用户可以创建user namespace,在这个user namespace中,该用户拥有所有的权限,包括在这个namespace中创建其他类型的namespace。

Linux namespace主要包含:

  • UTS namespace
  • Mount namespace
  • IPC namespace
  • PID namespace
  • Network namespace
  • User namespace

UTS namespace主要可以使得host拥有两套nodename与domainname:最大的区别就是如果使用lxc或者docker启动一个镜像,使用uname可以获得不同的主机名。

Mount namespace使得不同namespace中的相同进程都对文件结构有不同的视角,比起chroot有更好的安全性,使用namespace特性,可以使得系统的mount namespace产生主从的结构。

IPC namespace 使得每个IPC内的进程相互通信。

PID namespace最为神奇,他使得进程在不同的PID namespace中可以拥有相同的PID,也就是说如果系统拥有root namespace、namespace A、namespace B 。其中A、B是root的子命名空间,也就是说在A、B命名空间内部可以存在两个相同的PID,比如init 。当然这两个init的PID号从root命名空间来看是不同的,这里就有了映射的概念。

Network namespace 可以使得一个物理主机的网卡,模拟出两个虚拟网卡,每个虚拟网卡都可以绑定相同的端口,访问却不受影响。

User namespace 是在linux 3.8 才完成的部分,他可以使得一个进程的User ID和group ID相比于命名空间内外不同。举例:在root namespace中的一个非特权进程在namespace A中可以是init 0 ,可以是一个特权进程!

以上这些空间可以使得linux建立起一个轻量级的虚拟机系统。比如lxc、docker。

 

参考:

http://lwn.net/Articles/531114/

IA32下高端内存页框(ZONE_HIGHMEM)永久内核映射的分析与实现

March 6th, 2015 by JasonLe's Tech 1,191 views

一、背景

在AMD64平台下,可用的线性地址空间远远大于目前安装的RAM大小,所以在这种体系中,ZONE_HIGHMEM总是空的。

而在32位平台上,Linux设计者必须找到一种方式允许内核使用所有4GB的RAM,如果IA32支持PAE的话,则使内核可以支持64GB的RAM。

IA32架构下,之所以有这些约束,主要是为了兼容前代固有的硬件架构(i386),例如DMA直接读取,i386 内存大于900MB的情况。

ZONE_HIGHMEM属于高于896MB的内存区域,需要通过高端内存页框分配的方式映射到内核线性地址空间,从而使得内核可以对高端内存进行访问。而低于896MB直接通过(PAGE_OFFSET)偏移映射到物理内存上。详细查看http://www.lizhaozhong.info/archives/1193

二、原理

高端内存页框分配alloc_pages()函数返回的是page frame 页描述符(page description)的线性地址,因为这些页描述符地址一旦被分配,便不再改变,所以他是全局唯一的。这些页描述符在free_area_init_nodes()初始化时,已建立好。

注:free_area_init_nodes()之前的初始化与体系结构相关,当系统调用这个函数后,开始初始化每个pg_data_t的结构体,便于体系结构再无相关,http://lxr.free-electrons.com/source/mm/page_alloc.c#L5386。。

页描述符我们可以想象成系统将所有内存按照4k等分划分形成的数组下标。所有的page descriptor组成一个大的连续的数组 ,每个节点起始地址存放在 struct page *node_mem_map中,因此如果知道了page descriptor的地址pd,pd-node_mem_map就得到了pd是哪个page frame的描述符,也可以知道这个页描述符是否高于896MB。

 

已知alloc_pages()函数分配一个page descriptor的线性地址,可由它得到它所描述的物理页是整个内存的第几页:

假设是第N个物理页,那么这个物理页的物理地址是physAddr = N << PAGE_SHIFT   ,一般情况PAGE_SHIFT   为12 也就是4k。在得知该物理页的物理地址是physAddr后,就可以视physAddr的大小得到它的虚拟地址,然后对这个虚拟地址进行判断:

1.physAddr < 896M  对应虚拟地址是 physAddr + PAGE_OFFSET   (PAGE_OFFSET=3G)
2.physAddr >= 896M 对应虚拟地址不是静态映射的,通过内核的高端虚拟地址映射得到一个虚拟地址(也就是内核页表映射)。

在得到该页的虚拟地址之后,内核就可以正常访问这个物理页了。

所以,我们可以将physAddr高于896M的这128M看做成为一个内核页表,这个内核页表在paging_init()初始化完成。

三、实现

内核采取三种不同的机制将page frame映射到高端内存:永久内核映射、临时内核映射、非连续内存分配。

alloc_page()函数集返回的是全是struct page* 类型的数据,返回的也都是page descriptor的线性地址。然后系统要判断该page descriptor是否是highmem,也就是void *kmap(struct page *page)
http://lxr.free-electrons.com/source/arch/x86/mm/highmem_32.c#L6 。

 

系统调用PageHighMem(page),判断当前page是否是highmem,如果是返回kmap_high(page)。如果不是返回page_address(page)  http://lxr.free-electrons.com/source/mm/highmem.c#L412

 

void *kmap(struct page *page)
{
         might_sleep();
         if (!PageHighMem(page))
                 return page_address(page);
         return kmap_high(page);
}
void *page_address(const struct page *page)
{
         unsigned long flags;
         void *ret;
         struct page_address_slot *pas;

         if (!PageHighMem(page))
                 return lowmem_page_address(page);

         pas = page_slot(page);
         ret = NULL;
         spin_lock_irqsave(&pas->lock, flags);
         if (!list_empty(&pas->lh)) {
                 struct page_address_map *pam;

                 list_for_each_entry(pam, &pas->lh, list) {
                         if (pam->page == page) {
                                 ret = pam->virtual;
                                 goto done;
                         }
                 }
         }
 done:
         spin_unlock_irqrestore(&pas->lock, flags);
         return ret;
}

在page_address()函数中,我们要再次检查该page是否属于HIGHMEM,如果不属于,则直接计算偏移量__va(PFN_PHYS(page_to_pfn(page)))。

而pas = page_slot(page);之后的代码,是在永久映射中,系统为了方便查找,建立了一个page_address_htable散列表,系统可以很快的对于已经存放在散列表中的永久映射的page descriptor的线性地址进行查找,如果找到就返回内核线性地址,否则为NULL。

如果判断该page属于HIGHMEM,进入kmap_high(page) http://lxr.free-electrons.com/source/mm/highmem.c#L279

void *kmap_high(struct page *page)
{
         unsigned long vaddr;

/*
* For highmem pages, we can't trust "virtual" until
* after we have the lock.
*/
         lock_kmap();
         vaddr = (unsigned long)page_address(page);
         if (!vaddr)
                 vaddr = map_new_virtual(page);
         pkmap_count[PKMAP_NR(vaddr)]++;
         BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
         unlock_kmap();
         return (void*) vaddr;
}

虽然判断了HIGHMEM,调用这个函数,但是我们不能信任,仍需要再次检查,顺便检查是否在page_address_htable散列表中存在该page,如果不存在,则进行映射( map_new_virtual(page)),然后将pkmap_count数组中,这个page引用+1,然会内核就可以正常使用这个高端内存的线性地址了。

map_new_virtual()函数是进行HIGHMEM映射的核心函数,其中for(;;)循环是寻找可用的内核页表项进行高端内存页框映射,这里涉及到了页表color问题,主要为了提速查找效率。

当系统找到可用的页表项时,从line261~line268就是核心

  • 以PKMAP_BASE为基址last_pkmap_nr为偏移在永久映射区创建内核线性地址vaddr
  • 将vaddr加入pkmap_page_table
  • 引用赋值为1,之后有可能会++
  • 然后将该内核线性地址vaddr加入page,并返回内核线性地址
</pre>
<pre>217 static inline unsigned long map_new_virtual(struct page *page)
218 {
...
224 start:
225         count = get_pkmap_entries_count(color);
226         /* Find an empty entry */
227         for (;;) {
228                 last_pkmap_nr = get_next_pkmap_nr(color);
229                 if (no_more_pkmaps(last_pkmap_nr, color)) {
230                         flush_all_zero_pkmaps();
231                         count = get_pkmap_entries_count(color);
232                 }
233                 if (!pkmap_count[last_pkmap_nr])
234                         break;  /* Found a usable entry */
235                 if (--count)
236                         continue;
237
238                 /*
239                  * Sleep for somebody else to unmap their entries
240                  */
241                 {
242                         DECLARE_WAITQUEUE(wait, current);
243                         wait_queue_head_t *pkmap_map_wait =
244                                 get_pkmap_wait_queue_head(color);
246                         __set_current_state(TASK_UNINTERRUPTIBLE);
247                         add_wait_queue(pkmap_map_wait, &wait);
248                         unlock_kmap();
249                         schedule();
250                         remove_wait_queue(pkmap_map_wait, &wait);
251                         lock_kmap();
253                         /* Somebody else might have mapped it while we slept */
254                         if (page_address(page))
255                                 return (unsigned long)page_address(page);
256
257                         /* Re-start */
258                         goto start;
259                 }
260         }
261         vaddr = PKMAP_ADDR(last_pkmap_nr);
262         set_pte_at(&init_mm, vaddr,
263                    &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
264
265         pkmap_count[last_pkmap_nr] = 1;
266         set_page_address(page, (void *)vaddr);
268         return vaddr;

中间line241~259 内核考虑一种情况:如果这个PKMAP都已经被映射,最少的count都是>=1的,那么需要将当前的映射操作阻塞,然后加入等待队列,然后主动让出cpu被调度,直到PKMAP中存在count=0的页表项存在,唤醒,重新执行一次。

pkmap_count引用的可能值:0/1/>1。

0意味着该内核页表项未被映射高端内存,是可用的

1意味着虽然未被映射高端内存,但是需要unmapped,如果没有任何可用的页表项了,需要调用flush_all_zero_pkmaps()刷新pkmap_count,并置零。

>1意味着该页表项被使用。

 

参考:

http://repo.hackerzvoice.net/depot_madchat/ebooks/Mem_virtuelle/linux-mm/zonealloc.html#INITIALIZE

http://blog.csdn.net/lcw_202/article/details/5955783

https://www.kernel.org/doc/gorman/html/understand/understand006.html#sec: Mapping addresses to struct pages

物理内存管理:伙伴系统数据结构分析

March 4th, 2015 by JasonLe's Tech 1,430 views

伙伴系统(buddy system)在物理内存管理中占有重要地位。

我们知道物理内存被分为三大部分:DMA、NORMAL、HIGHMEN三个区域。每个内存域都有一个struct zone的实例。而这个struct zone 的实例又被struct pglist_data管理。具体看http://www.lizhaozhong.info/archives/1184

page frame <==> struct page

struct zone 这个结构体非常庞大,总的来说分成三部分,每一部分由 ZONE_PADDING(_pad1_)这种标示分割。我们可以通过struct zone 下的unsigned long zone_start_pfn 找到某个特定的page!但是这种page frame,我们无法知道空闲页框的布局,不利于分配page frame。

具体zone定义:http://lxr.free-electrons.com/source/include/linux/mmzone.h#L327

为了尽量分配连续的page frame,避免外部碎片的产生,伙伴系统(buddy system)满足这个需求。
每个内存域(struct zone)的实例都有free_area数组,这个数组大小是11,也就是说空闲区域有0-10阶的page frame 块链表。比如free_area[3]所对应的页框块链表中,每个节点对应8个连续的页框(2的3次方)。

#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
struct zone{
........
476         ZONE_PADDING(_pad1_)
477
478         /* Write-intensive fields used from the page allocator */
479         spinlock_t              lock;
480
481         /* free areas of different sizes */
482         struct free_area        free_area[MAX_ORDER];
483
484         /* zone flags, see below */
485         unsigned long           flags;
486
487         ZONE_PADDING(_pad2_)
......
}

而struct free_area如下http://lxr.free-electrons.com/source/include/linux/mmzone.h#L92

 92 struct free_area {
 93         struct list_head        free_list[MIGRATE_TYPES];
 94         unsigned long           nr_free;
 95 };

通过声明看到free_list是一个链表数组。nr_free表示当前链表中空闲页框块的数目,比如free_area[3]中nr_free的值为5,表示有5个大小为8的页框块,那么总的页框数目为40。

具体概述上面buddy system结构就是:
buddy

在/proc中我们可以查看到每个阶空闲大小的PFN数量(注:本机使用的是AMD64系统,在AMD64中没有ZONE_HIGHMEN,ZONE_DMA寻值为16M,ZONE_DMA32寻值为0-4GiB,在32为机器上DMA32为0)

[root@localhost cgroup_unified]# cat /proc/buddyinfo
Node 0, zone      DMA      8      7      6      5      4      2      1      2      2      3      0
Node 0, zone    DMA32   1059   1278   9282   1868   1611    260     52     20      7      2      1

在struct list_head free_list[MIGRATE_TYPES]中,我们发现每个阶都带有一个MIGRATE_TYPES标志,通过这种方式,系统又把每个阶的空闲page更加详细的分割,具体类型有不可移动,移动,保留等。

 38 #define MIGRATE_UNMOVABLE     0
 39 #define MIGRATE_RECLAIMABLE   1
 40 #define MIGRATE_MOVABLE       2
 41 #define MIGRATE_PCPTYPES      3 /* the number of types on the pcp lists */
 42 #define MIGRATE_RESERVE       3
 43 #define MIGRATE_ISOLATE       4 /* can't allocate from here */
 44 #define MIGRATE_TYPES         5

这是为了更大限度的满足连续物理页框的需要,如果要分配一种MIGRATE_UNMOVABLE类型的页框,而两边的页框是可以移动的,这样就限制了连续大页框的分配,产生了外部碎片。
使用MIGRATE_TYPES策略后,不可移动的页面的不可移动性仅仅影响它自身的类别而不会导致一个不可移动的页面两边都是可移动的页面。这就是MIGRATE_TYPE被引入的目的。

MIGRATE_TYPE限制了内存页面的分配地点从而避免碎片,而不再仅仅寄希望于它们被释放时通过合并避免碎片[1]。

这种策略在proc中也可以查看:

[root@localhost cgroup_unified]# cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10
Node    0, zone      DMA, type    Unmovable      1      4      4      1      1      1      1      1      0      0      0
Node    0, zone      DMA, type  Reclaimable      5      1      1      0      0      0      0      1      1      1      0
Node    0, zone      DMA, type      Movable      2      0      2      4      3      1      0      0      1      1      0
Node    0, zone      DMA, type      Reserve      0      0      0      0      0      0      0      0      0      1      0
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0
Node    0, zone    DMA32, type    Unmovable      1      0      8      1     17     15      5      0      0      0      0
Node    0, zone    DMA32, type  Reclaimable   1041    162      1      0      1      1      1      1      1      0      0
Node    0, zone    DMA32, type      Movable    698     78      7   1221   1194    232     45     20      6      0      0
Node    0, zone    DMA32, type      Reserve      0      0      0      0      0      0      0      0      0      1      1
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 

Number of blocks type     Unmovable  Reclaimable      Movable      Reserve      Isolate
Node 0, zone      DMA            1            2            4            1            0
Node 0, zone    DMA32           89           93         1216            2            0

参考:

[1] http://blog.csdn.net/dog250/article/details/6108028

cgroup 介绍(2)

March 2nd, 2015 by JasonLe's Tech 1,106 views

之前在第一篇介绍cgroup的文章中,我初步使用cgroup对资源进行限制隔离http://www.lizhaozhong.info/archives/1211

但是基于层级的cgroup存在一个弊端:就是不灵活,树的深度可能是无限的,这就导致实际操作中管理非常繁琐。

基于这个原因,在kernel 3.16中正式加入了unified  hierarchy特性,这个特性目前仍然在开发,所以如果想显式开启该特性需要
mount -t cgroup -o __DEVEL__sane_behavior cgroup $MOUNT_POINT

__DEVEL__sane_behavior通过看名字,我们也能发现这个特性仍然在开发。

在之前的cgroup hierarchy中,我们知道一个hierarchy可以绑定一个子系统,也可以同时绑定12个子系统。

举例层级A绑定cpuset,层级B绑定memory,如果有一个task同时需要这两个子系统,则很多时候task在这两个层级中存在正交,非常不便。

hierarchy may be collapsed from leaf towards root when viewed from specific
controllers.  For example, a given configuration might not care about
how memory is distributed beyond a certain level while still wanting
to control how CPU cycles are distributed.

 

如果我们开启__DEVEL__sane_behavior特性,我们看到cgroup.controllers 存在的子系统,在unified hierarchy中,系统会把所有子系统都挂载到根层级下,只有leaf节点可以存在tasks,非叶子节点只进行资源控制。

# mount -t cgroup -o __DEVEL__sane_behavior cgroup /sys/fs/cgroup
# cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu cpuacct memory devices freezer net_cls blkio perf_event net_prio hugetlb

 

现在我们在root cgroup下面创建parent与child,根层级的cgroup.subtree_control 控制parents的cgroup.controllers

如此往复,上级的cgroup.subtree_control控制下级的cgroup.controllers,也就是说subsystem不会有传递性!

如下面的例子,如果我指定根层级的cgroup.subtree_control 可以使能memory与cpu两个子系统,也就是说parents中可以控制memory、cpu两个子系统。而child如果没有指定子系统,是不会控制memory与cpu的。

# mkdir /sys/fs/cgroup/parent
# mkdir /sys/fs/cgroup/parent/child</pre>
# echo "+memory +cpu" > /sys/fs/cgroup/cgroup.subtree_control
# cat /sys/fs/cgroup/parent/cgroup.controllers
cpu memory

举个例子:

A(b,m) - B(b,m) - C (b)
              \ - D (b) - E

其中b代表blkio,m代表memory,A是根,在这个结构中ACD都拥有进程,比如C对blkio受限,那么memory则不受限,共享B,E比较特殊,如果没有指定子系统,那么blkio受D控制,memory受B控制。具体操作方式在上面parents、child已声明。

如果该cgroup中已有进程,那么只有在关联的组没有包含进程的时候,cgroup.subtree_control文件能被用来改变控制器的设置。

中间层级必须拥有子系统,如果指定E受限于blkio,那么系统不承认该操作!

Unified hierarchy implements an interface file “cgroup.populated”which can be used to monitor whether the cgroup’s subhierarchy has tasks in it or not. Its value is 0 if there is no task in the cgroup and its descendants; otherwise, 1. poll and [id]notify events are triggered when the value changes.

其他unified hierarchy 改动在document中说的很清楚,这里不再赘述。

包括tasks,cgroup.procs,cgroup.clone_children会被移除等。一旦这种层级开发明确,旧有的cgroup机制会被这种unified hierarchy代替。

 

参考:

http://lwn.net/Articles/601840/

http://events.linuxfoundation.org/sites/events/files/slides/2014-KLF.pdf

https://www.kernel.org/doc/Documentation/cgroups/unified-hierarchy.txt

http://d.hatena.ne.jp/defiant/mobile?date=20140826