Offline Memory gets stuck in offline_pages()
Hi all
Overview:
During testing the CXL memory hotremove, we noticed that daxctl offline-memory dax0.0 would get stuck forever sometimes. daxctl offline-memory dax0.0 will write "offline" to /sys/devices/system/memory/memoryNNN/state.
Workaround:
When it happens, we can type Ctrl-C to abort it and then retry again. Then the CXL memory is able to offline successfully.
Where the kernel gets stuck:
After digging into the kernel, we found that when the issue occurs, the kernel is stuck in the outer loop of offline_pages(). Below is a piece of the highlighted offline_pages():
```
int __ref offline_pages()
{
do { // outer loop
pfn = start_pfn;
do {
ret = scan_movable_pages(pfn, end_pfn, &pfn); // It returns -ENOENT
if (!ret)
do_migrate_range(pfn, end_pfn); // Not reach here
} while (!ret);
ret = test_pages_isolated(start_pfn, end_pfn, MEMORY_OFFLINE);
} while (ret); // ret is -EBUSY
}
```In this case, we dumped the first page that cannot be isolated (see dump_page below), it's content does not change in each iteration.:
``` Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page... ```
Every time the issue occurs, the content of the page structure is similar.
Questions:
Q1. Is this behavior expected? At least for an OS administrator, it should return
promptly (success or failure) instead of hanging indefinitely.
Q2. Regarding the offline_pages() function, encountering such a page indeed causes
an endless loop. Shouldn't another part of the kernel timely changed the state of this page?
When I use the workaround mentioned above (Ctrl-C and try offline again), I find that the page state changes (see dump_page below):
``` Jun 28 15:33:12 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:33:12 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:33:12 linux kernel: raw: 009fffffc0000000 dead000000000100 dead000000000122 0000000000000000 Jun 28 15:33:12 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:33:12 linux kernel: page dumped because: previous trouble page ```
What our test does:
We have a CXL memory device, which is configured as kmem and online into the MOVABLE zone as NUMA node2. We run two processes, consume-memory and offline-memory, in parallel, see the pseudo code below:
```
main()
{
if (fork() == 0)
numactl -m 2 ./consume-memory
else {
daxctl offline-memory dax0.0
wait()
}
}
```Attached is the process information (when it gets stuck):
``` root 25716 0.0 0.0 2460 1408 pts/0 S+ 15:28 0:00 ./main root 25719 0.0 0.0 0 0 pts/0 Z+ 15:28 0:00 [consume-memory] <defunct> root 25720 98.6 0.0 9476 3740 pts/0 R+ 15:28 0:26 daxctl offline-memory /dev/dax0.0 ```
Feel free to let me know if you need more details.
Thank you for your attention to this issue. Looking forward to your insights.
Thanks
Zhijian
Quoted 7 lines · Zhijian Li (Fujitsu)
Hi all
Overview:
During testing the CXL memory hotremove, we noticed thatdaxctl offline-memory dax0.0would get stuck forever sometimes. daxctl offline-memory dax0.0 will write "offline" to/sys/devices/system/memory/memoryNNN/state.
Hi,
See
Documentation/admin-guide/mm/memory-hotplug.rst
"
Further, when running into out of memory situations while migrating pages, or when still encountering permanently unmovable pages within ZONE_MOVABLE (-> BUG), memory offlining will keep retrying until it eventually succeeds.
When offlining is triggered from user space, the offlining context can be terminated by sending a signal. A timeout based offlining can easily be implemented via::
% timeout $TIMEOUT offline_block | failure_handling
"
Quoted 25 lines
Workaround:
When it happens, we can type Ctrl-C to abort it and then retry again. Then the CXL memory is able to offline successfully.Where the kernel gets stuck:
After digging into the kernel, we found that when the issue occurs, the kernel is stuck in the outer loop ofoffline_pages(). Below is a piece of the highlightedoffline_pages():``` int __ref offline_pages() { do { // outer loop pfn = start_pfn; do { ret = scan_movable_pages(pfn, end_pfn, &pfn); // It returns -ENOENT if (!ret) do_migrate_range(pfn, end_pfn); // Not reach here } while (!ret); ret = test_pages_isolated(start_pfn, end_pfn, MEMORY_OFFLINE); } while (ret); // ret is -EBUSY } ```
Quoted 8 lines
In this case, we dumped the first page that cannot be isolated (see
dump_pagebelow), it's content does not change in each iteration.:
```Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page...
Are you sure that's the problematic page?
refcount:0
Indicates that the page is free. But maybe it does not have PageBuddy() set.
It could also be that this is a "tail" page of a PageBuddy() page, and somehow we always end up on the tail in test_pages_isolated().
Which kernel + architecture are you testing with?
Quoted 7 lines
```
Every time the issue occurs, the content of the page structure is similar.
Questions:
Q1. Is this behavior expected? At least for an OS administrator, it should return
promptly (success or failure) instead of hanging indefinitely.
It's expected that it might take a long time (possibly forever) in corner cases. See documentation.
But it's likely unexpected that we have some problematic page here.
Quoted 3 lines
Q2. Regarding the
offline_pages()function, encountering such a page indeed causes
an endless loop. Shouldn't another part of the kernel timely changed the state of this page?
There are various things that can go wrong. One issue might be that we try migrating a page but continuously fail to allocate memory to be used as a migration target. It seems unlikely with the page you dumped above, though.
Do you maybe have that CXL memory be on a separate "fake" NUMA node, and your workload mbind() itself to that NUMA node, possibly refusing to migrate somewhere else?
Quoted 16 lines
When I use the workaround mentioned above (Ctrl-C and try offline again), I find that the page state changes (see
dump_pagebelow):``` Jun 28 15:33:12 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:33:12 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:33:12 linux kernel: raw: 009fffffc0000000 dead000000000100 dead000000000122 0000000000000000 Jun 28 15:33:12 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:33:12 linux kernel: page dumped because: previous trouble page ```What our test does:
We have a CXL memory device, which is configured as kmem and online into the MOVABLE zone as NUMA node2. We run two processes, consume-memory and offline-memory, in parallel, see the pseudo code below:
Quoted 5 lines
```
main()
{
if (fork()== 0)
numactl -m 2 ./consume-memory
What exactly does "consume-memory" do? Does it involve hugetlb maybe?
Signature · 3 lines
Cheers, David / dhildenb
Quoted 9 lines · David Hildenbrand
Quoted 5 lines · Zhijian Li (Fujitsu)
Hi all
Overview:
During testing the CXL memory hotremove, we noticed thatdaxctl offline-memory dax0.0would get stuck forever sometimes. daxctl offline-memory dax0.0 will write "offline" to/sys/devices/system/memory/memoryNNN/state.Hi,
SeeDocumentation/admin-guide/mm/memory-hotplug.rst
Many thanks for this quotation.
It reminds me that we encountered OOM during the test sometimes.
Quoted 39 lines
"
Further, when running into out of memory situations while migrating pages, or when still encountering permanently unmovable pages withinZONE_MOVABLE(-> BUG), memory offlining will keep retrying until it eventually succeeds.
When offlining is triggered from user space, the offlining context can be terminated by sending a signal. A timeout based offlining can easily be implemented via::
% timeout $TIMEOUToffline_block|failure_handling
"Quoted 21 lines
Workaround:
When it happens, we can type Ctrl-C to abort it and then retry again. Then the CXL memory is able to offline successfully.
Where the kernel gets stuck:
After digging into the kernel, we found that when the issue occurs, the kernel is stuck in the outer loop ofoffline_pages(). Below is a piece of the highlightedoffline_pages():``` int __ref offline_pages() { do { // outer loop pfn = start_pfn; do { ret = scan_movable_pages(pfn, end_pfn, &pfn); // It returns -ENOENT if (!ret) do_migrate_range(pfn, end_pfn); // Not reach here } while (!ret); ret = test_pages_isolated(start_pfn, end_pfn, MEMORY_OFFLINE); } while (ret); // ret is -EBUSY } ```Quoted 8 lines
In this case, we dumped the first page that cannot be isolated (see
dump_pagebelow), it's content does not change in each iteration.:
```Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page...
Quoted 1 line
Are you sure that's the problematic page?
Yes, I dumped the page in the else in __test_page_isolated_in_pageblock(), see below
573 __test_page_isolated_in_pageblock(unsigned long pfn, unsigned long end_pfn, 574 int flags)
575 {
576 struct page *page;
577
578 while (pfn < end_pfn) {
579 page = pfn_to_page(pfn);
580 if (PageBuddy(page))
581 /*
582 * If the page is on a free list, it has to be on 583 * the correct MIGRATE_ISOLATE freelist. There is no 584 * simple way to verify that as VM_BUG_ON(), though. 585 */
586 pfn += 1 << buddy_order(page);
587 else if ((flags & MEMORY_OFFLINE) && PageHWPoison(page)) 588 /* A HWPoisoned page cannot be also PageBuddy */ 589 pfn++;
590 else if ((flags & MEMORY_OFFLINE) && PageOffline(page) && 591 !page_count(page))
592 /*
593 * The responsible driver agreed to skip PageOffline() 594 * pages when offlining memory by dropping its 595 * reference in MEM_GOING_OFFLINE.
596 */
597 pfn++;
598 else
/****************** dump_page(page) here ****************/
599 break;
600 }
601
602 return pfn;
603 }
We also dumped that page at the beginning of offline_pages(), it had the same page structure content. IOW, this page has been problematic before the loop.
Quoted 3 lines
refcount:0
Indicates that the page is free. But maybe it does not havePageBuddy()set. It could also be that this is a "tail" page of aPageBuddy()page,
It doesn't seem it's the tail page of the PageBuddy(), I also tested it that it didn't covered by the buddy_order(page) of the previous pageBuddy.
Quoted 3 lines
and
somehow we always end up on the tail intest_pages_isolated(). Which kernel + architecture are you testing with?
This test is running on QEMU/tcg x86_64 guest with kernel v6.10-rc2, the host is x86_64.
/home/lizhijian/qemu/build/qemu-system-x86_64 \
-name guest=fedora-37-client \
-nographic \
-machine pc-q35-3.1,accel=tcg,nvdimm=on,cxl=on \
-cpu qemu64 \
-smp 4,sockets=4,cores=1,threads=1 \
-m size=8G,slots=8,maxmem=19922944k \
-hda ./Fedora-Server-1.qcow2 \
-object memory-backend-ram,size=4G,id=m0 \
-object memory-backend-ram,size=4G,id=m1 \
-numa node,nodeid=0,cpus=0-1,memdev=m0 \
-numa node,nodeid=1,cpus=2-3,memdev=m1 \
-device pxb-cxl,bus_nr=12,bus=pcie.0,id=cxl.1 \
-device cxl-rp,port=0,bus=cxl.1,id=root_port13,chassis=0,slot=2 \
-object memory-backend-ram,size=2G,share=on,id=vmem0 \
-device cxl-type3,bus=root_port13,volatile-memdev=vmem0,id=type3-cxl-vmem0 \
-M cxl-fmw.0.targets.0=cxl.1,cxl-fmw.0.size=32G,cxl-fmw.0.interleave-granularity=4k \
Quoted 22 lines
Quoted 10 lines
```
Every time the issue occurs, the content of the page structure is similar.
Questions:
Q1. Is this behavior expected? At least for an OS administrator, it should return
promptly (success or failure) instead of hanging indefinitely.It's expected that it might take a long time (possibly forever) in corner cases. See documentation.
But it's likely unexpected that we have some problematic page here.
Quoted 5 lines
Q2. Regarding the
offline_pages()function, encountering such a page indeed causes
an endless loop. Shouldn't another part of the kernel timely changed the state
of this page?
Quoted 6 lines
There are various things that can go wrong. One issue might be that we try migrating a page but continuously fail to allocate memory to be used as a migration target. It seems unlikely with the page you dumped above, though.
Do you maybe have that CXL memory be on a separate "fake" NUMA node,
Yes, it's a memory only(CPU less) node.
``` [root@localhost guest]# numactl -H available: 3 nodes (0-2) node 0 cpus: 0 1 node 0 size: 3927 MB node 0 free: 3430 MB node 1 cpus: 2 3 node 1 size: 4028 MB node 1 free: 3620 MB node 2 cpus: node 2 size: 0 MB node 2 free: 0 MB node distances: node 0 1 2 0: 10 20 20 1: 20 10 20 2: 20 20 10 [root@localhost guest]# daxctl online-memory dax0.0 --movable onlined memory for 1 device [root@localhost guest]# numactl -H available: 3 nodes (0-2) node 0 cpus: 0 1 node 0 size: 3927 MB node 0 free: 3449 MB node 1 cpus: 2 3 node 1 size: 4028 MB node 1 free: 3614 MB node 2 cpus: node 2 size: 2048 MB node 2 free: 2048 MB node distances: node 0 1 2 0: 10 20 20 1: 20 10 20 2: 20 20 10 ```
Quoted 2 lines
and your workload
mbind()itself to that NUMA node, possibly refusing to migrate somewhere else?
In most testing runs, we do see the pages migrate to other node when we trigger a offline memory.
Quoted 24 lines
Quoted 24 lines
When I use the workaround mentioned above (Ctrl-C and try offline again), I find
that the page state changes (seedump_pagebelow):``` Jun 28 15:33:12 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:33:12 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:33:12 linux kernel: raw: 009fffffc0000000 dead000000000100 dead000000000122 0000000000000000 Jun 28 15:33:12 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:33:12 linux kernel: page dumped because: previous trouble page ```What our test does:
We have a CXL memory device, which is configured as kmem and online into the MOVABLE
zone as NUMA node2. We run two processes, consume-memory and offline-memory, in parallel,
see the pseudo code below:
Quoted 7 lines
Quoted 5 lines
```
main()
{
if (fork()== 0)
numactl -m 2 ./consume-memoryWhat exactly does "consume-memory" do? Does it involve hugetlb maybe?
No, they are just malloc() pages, see the code as below. We did the 2M hugetlb pattern, the hugetlb pattern will get offlined success or fail with EBUSY promptly.
```
int main(int argc, char **argv) {
unsigned long long mem_size = 0;
if (argc < 2) {
printf("please specify the mem size in MB!\n");
return -1;
}
mem_size = strtoull(argv[1], NULL, 10);
if (mem_size <= 0) {
printf("invalid mem size '%s'\n", argv[1]);
return -1;
}
printf("the mem size is %llu MB\n", mem_size);
mem_size *= 1024 * 1024;
char * a = (char *)malloc(mem_size);
if (!a) {
printf("malloc failed\n");
return -1;
}
memset(a, 0, mem_size);
return 0;
}
```Feel free to let me know if you want to add some trace/debug in the code to do a further check.
Thanks
Zhijian
Quoted 2 lines
All,
Some progress updates
When issue occurs, calling __drain_all_pages() can make offline_pages() escape from the loop.
Quoted 7 lines
Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page...
With this problematic page structure contents, it seems that the list_head = {ffffdfbd9e603788, ffffd4f0ffd97ef0} is valid.
I guess it was linking to the pcp_list, so I dumped the per_cpu_pages[cpu].count in every in critical timings.
An example is as below,offline_pages()
{
// per_cpu_pages[1].count = 0
zone_pcp_disable() // will call __drain_all_pages()
// per_cpu_pages[1].count = 188
do {
do {
scan_movable_pages()
ret = do_migrate_range()
} while (!ret)
ret = test_pages_isolated()
if(is the 1st iteration)
// per_cpu_pages[1].count = 182
if (issue occurs) { /* if the loop take beyond 10 seconds */
// per_cpu_pages[1].count = 61
__drain_all_pages()
// per_cpu_pages[1].count = 0
/* will escape from the outer loop in later iterations */
}
} while (ret)
}Some interesting points:
- After the 1st __drain_all_pages(), per_cpu_pages[1].count increased to 188 from 0, does it mean it's racing with something...?
- per_cpu_pages[1].count will decrease but not decrease to 0 during iterations
- when issue occurs, calling __drain_all_pages() will decrease per_cpu_pages[1].count to 0.
So I wonder if it's fine to call __drain_all_pages() in the loop?
Looking forward to your insights.
Thanks
Zhijian
Quoted 90 lines · Zhijian Li (Fujitsu)
Hi all
Overview:
During testing the CXL memory hotremove, we noticed thatdaxctl offline-memory dax0.0would get stuck forever sometimes. daxctl offline-memory dax0.0 will write "offline" to/sys/devices/system/memory/memoryNNN/state.Workaround:
When it happens, we can type Ctrl-C to abort it and then retry again. Then the CXL memory is able to offline successfully.Where the kernel gets stuck:
After digging into the kernel, we found that when the issue occurs, the kernel is stuck in the outer loop ofoffline_pages(). Below is a piece of the highlightedoffline_pages():``` int __ref offline_pages() { do { // outer loop pfn = start_pfn; do { ret = scan_movable_pages(pfn, end_pfn, &pfn); // It returns -ENOENT if (!ret) do_migrate_range(pfn, end_pfn); // Not reach here } while (!ret); ret = test_pages_isolated(start_pfn, end_pfn, MEMORY_OFFLINE); } while (ret); // ret is -EBUSY } ```In this case, we dumped the first page that cannot be isolated (see
dump_pagebelow), it's content does not change in each iteration.:``` Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page... ```Every time the issue occurs, the content of the page structure is similar.
Questions:
Q1. Is this behavior expected? At least for an OS administrator, it should return
promptly (success or failure) instead of hanging indefinitely.
Q2. Regarding theoffline_pages()function, encountering such a page indeed causes
an endless loop. Shouldn't another part of the kernel timely changed the state of this page?When I use the workaround mentioned above (Ctrl-C and try offline again), I find that the page state changes (see
dump_pagebelow):``` Jun 28 15:33:12 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:33:12 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:33:12 linux kernel: raw: 009fffffc0000000 dead000000000100 dead000000000122 0000000000000000 Jun 28 15:33:12 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:33:12 linux kernel: page dumped because: previous trouble page ```What our test does:
We have a CXL memory device, which is configured as kmem and online into the MOVABLE zone as NUMA node2. We run two processes, consume-memory and offline-memory, in parallel, see the pseudo code below:``` main() { if (fork() == 0) numactl -m 2 ./consume-memory else { daxctl offline-memory dax0.0 wait() } } ```Attached is the process information (when it gets stuck):
``` root 25716 0.0 0.0 2460 1408 pts/0 S+ 15:28 0:00 ./main root 25719 0.0 0.0 0 0 pts/0 Z+ 15:28 0:00 [consume-memory] <defunct> root 25720 98.6 0.0 9476 3740 pts/0 R+ 15:28 0:26 daxctl offline-memory /dev/dax0.0 ```Feel free to let me know if you need more details.
Thank you for your attention to this issue. Looking forward to your insights.Thanks
Zhijian
Quoted 14 lines · Zhijian Li (Fujitsu)
All,
Some progress updates
When issue occurs, calling
__drain_all_pages()can makeoffline_pages()escape from the loop.Quoted 7 lines
Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788 ffffd4f0ffd97ef0 0000000000000000 Jun 28 15:29:26 linux kernel: raw: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000 Jun 28 15:29:26 linux kernel: page dumped because: trouble page...
Quoted 5 lines
With this problematic page structure contents, it seems that the
list_head= {ffffdfbd9e603788,ffffd4f0ffd97ef0} is valid.I guess it was linking to the
pcp_list, so I dumped the per_cpu_pages[cpu].count in every in critical timings.
So, is your reproducer getting fixed when you call __drain_all_pages() in the loop? (not that it's the right fix, but a could datapoint :) )
Quoted 27 lines
An example is as below,
offline_pages(){ // per_cpu_pages[1].count = 0 zone_pcp_disable() // will call __drain_all_pages() // per_cpu_pages[1].count = 188 do { do { scan_movable_pages() ret = do_migrate_range() } while (!ret) ret = test_pages_isolated() if(is the 1st iteration) // per_cpu_pages[1].count = 182 if (issue occurs) { /* if the loop take beyond 10 seconds */ // per_cpu_pages[1].count = 61 __drain_all_pages() // per_cpu_pages[1].count = 0 /* will escape from the outer loop in later iterations */ } } while (ret) }
Quoted 5 lines
Some interesting points:
- After the 1st__drain_all_pages(), per_cpu_pages[1].count increased to 188 from 0, does it mean it's racing with something...?
- per_cpu_pages[1].count will decrease but not decrease to 0 during iterations
- when issue occurs, calling__drain_all_pages()will decrease per_cpu_pages[1].count to 0.
That's indeed weird. Maybe there is a race, or zone_pcp_disable() is not fully effective for a zone?
Quoted 4 lines
So I wonder if it's fine to call
__drain_all_pages()in the loop?Looking forward to your insights.
So, in free_unref_page(), we make sure to never place MIGRATE_ISOLATE onto the PCP. All pageblocks we are going to offline should be isolated at this point, so no page that is getting freed and part of the to-be-offlined range should end up on the PCP. So far the theory.
In offlining code we do
1) Set MIGRATE_ISOLATE
2) zone_pcp_disable() -> set high-and-batch to 0 and drain
Could there be a race in free_unref_page(), such that although zone_pcp_disable() succeeds, we would still end up with a page in the pcp? (especially, one that has MIGRATE_ISOLATE set for its pageblock?)
Signature · 3 lines
Cheers, David / dhildenb
Quoted 26 lines
-----Original Message-----
From: David Hildenbrand <[email protected]>
Sent: Thursday, July 4, 2024 4:15 PMQuoted 5 lines · Zhijian Li (Fujitsu)
All,
Some progress updates
When issue occurs, calling
__drain_all_pages()can makeoffline_pages()escapefrom the loop.
Quoted 8 lines
Quoted 8 lines
Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788
ffffd4f0ffd97ef00000000000000000 Jun 28 15:29:26 linux kernel: raw:
0000000000000000 000000000000000000000000ffffffff0000000000000000Jun 28 15:29:26 linux kernel: page dumped because: trouble page...Quoted 2 lines
Quoted 1 line
Quoted 8 lines
Quoted 5 lines
With this problematic page structure contents, it seems that the
list_head= {ffffdfbd9e603788,ffffd4f0ffd97ef0} is valid.I guess it was linking to the
pcp_list, so I dumped the per_cpu_pages[cpu].count in every in critical timings.So, is your reproducer getting fixed when you call
__drain_all_pages()in the loop? (not that it's the right fix, but a could datapoint :) )
Yeah, it works for my reproducer.
Quoted 31 lines
Quoted 28 lines
An example is as below,
offline_pages(){ // per_cpu_pages[1].count = 0 zone_pcp_disable() // will call __drain_all_pages() // per_cpu_pages[1].count = 188 do { do { scan_movable_pages() ret = do_migrate_range() } while (!ret) ret = test_pages_isolated() if(is the 1st iteration) // per_cpu_pages[1].count = 182 if (issue occurs) { /* if the loop take beyond 10 seconds */ // per_cpu_pages[1].count = 61 __drain_all_pages() // per_cpu_pages[1].count = 0 /* will escape from the outer loop in later iterations */ } } while (ret) }Quoted 2 lines
Some interesting points:
- After the 1st__drain_all_pages(), per_cpu_pages[1].count increased to 188from 0,
Quoted 8 lines
Quoted 2 lines
does it mean it's racing with something...?
- per_cpu_pages[1].count will decrease but not decrease to 0 duringiterations
Quoted 1 line
- when issue occurs, calling
__drain_all_pages()will decreaseper_cpu_pages[1].count to 0.
That's indeed weird. Maybe there is a race, or
zone_pcp_disable()is not fully effective for a zone?
I often see there still are pages in PCP after the zone_pcp_disable().
Quoted 12 lines
Quoted 5 lines
So I wonder if it's fine to call
__drain_all_pages()in the loop?Looking forward to your insights.
So, in
free_unref_page(), we make sure to never placeMIGRATE_ISOLATEonto the PCP. All pageblocks we are going to offline should be isolated at this point, so no page that is getting freed and part of the to-be-offlined range should end up on the PCP. So far the theory.
Quoted 8 lines
In offlining code we do
1) Set
MIGRATE_ISOLATE
2)zone_pcp_disable()-> set high-and-batch to 0 and drainCould there be a race in
free_unref_page(), such that althoughzone_pcp_disable()succeeds, we would still end up with a page in the pcp? (especially, one that hasMIGRATE_ISOLATEset for its pageblock?)
Thanks for your idea, I will further investigate in this direction.
Thanks
Zhijian
Quoted 6 lines
--
Cheers,David / dhildenb
David && ALL,
Some progress updates
Quoted 95 lines · Zhijian Li (Fujitsu)
Quoted 26 lines
-----Original Message-----
From: David Hildenbrand <[email protected]>
Sent: Thursday, July 4, 2024 4:15 PMQuoted 5 lines · Zhijian Li (Fujitsu)
All,
Some progress updates
When issue occurs, calling
__drain_all_pages()can makeoffline_pages()escapefrom the loop.
Quoted 8 lines
Quoted 8 lines
Jun 28 15:29:26 linux kernel: page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x7980dd Jun 28 15:29:26 linux kernel: flags: 0x9fffffc0000000(node=2|zone=3|lastcpupid=0x1fffff) Jun 28 15:29:26 linux kernel: raw: 009fffffc0000000 ffffdfbd9e603788
ffffd4f0ffd97ef00000000000000000 Jun 28 15:29:26 linux kernel: raw:
0000000000000000 000000000000000000000000ffffffff0000000000000000Jun 28 15:29:26 linux kernel: page dumped because: trouble page...Quoted 2 lines
Quoted 1 line
Quoted 8 lines
Quoted 5 lines
With this problematic page structure contents, it seems that the
list_head= {ffffdfbd9e603788,ffffd4f0ffd97ef0} is valid.I guess it was linking to the
pcp_list, so I dumped the per_cpu_pages[cpu].count in every in critical timings.So, is your reproducer getting fixed when you call
__drain_all_pages()in the loop? (not that it's the right fix, but a could datapoint :) )Yeah, it works for my reproducer.
Quoted 31 lines
Quoted 28 lines
An example is as below,
offline_pages(){ // per_cpu_pages[1].count = 0 zone_pcp_disable() // will call __drain_all_pages() // per_cpu_pages[1].count = 188 do { do { scan_movable_pages() ret = do_migrate_range() } while (!ret) ret = test_pages_isolated() if(is the 1st iteration) // per_cpu_pages[1].count = 182 if (issue occurs) { /* if the loop take beyond 10 seconds */ // per_cpu_pages[1].count = 61 __drain_all_pages() // per_cpu_pages[1].count = 0 /* will escape from the outer loop in later iterations */ } } while (ret) }Quoted 2 lines
Some interesting points:
- After the 1st__drain_all_pages(), per_cpu_pages[1].count increased to 188from 0,
Quoted 8 lines
Quoted 2 lines
does it mean it's racing with something...?
- per_cpu_pages[1].count will decrease but not decrease to 0 duringiterations
Quoted 1 line
- when issue occurs, calling
__drain_all_pages()will decreaseper_cpu_pages[1].count to 0.
That's indeed weird. Maybe there is a race, or
zone_pcp_disable()is not fully effective for a zone?I often see there still are pages in PCP after the
zone_pcp_disable().Quoted 14 lines
Quoted 5 lines
So I wonder if it's fine to call
__drain_all_pages()in the loop?Looking forward to your insights.
So, in
free_unref_page(), we make sure to never placeMIGRATE_ISOLATEonto the PCP. All pageblocks we are going to offline should be isolated at this point, so no page that is getting freed and part of the to-be-offlined range should end up on the PCP. So far the theory.In offlining code we do
Quoted 8 lines
Quoted 6 lines
1) Set
MIGRATE_ISOLATE
2)zone_pcp_disable()-> set high-and-batch to 0 and drainCould there be a race in
free_unref_page(), such that althoughzone_pcp_disable()succeeds, we would still end up with a page in the pcp? (especially, one that hasMIGRATE_ISOLATEset for its pageblock?)Thanks for your idea, I will further investigate in this direction.
some updates
CPU0 CPU1
----------- ---------// erase pcp_listzone_pcp_disable // pcp->count = 0
lru_cache_diable() __rmqueue_pcplist() // re-add pages to pcp_lsit
__rmqueue_pcplist() // drop pages from pcp_list
decay_pcp_high() // drop pages from pcp_list
loop { ...
__rmqueue_pcplist() // drop pages from pcp_list,
// it will be only called a few times during the loop
scan_movable_pages() ...
migration_pages() decay_pcp_high() // drop pages from pcp_list, it will be called by
// a worker periodically during the loop
// wait pcp_list to be empty
} while (test_pages_isolated())
And we noticed that re-add pages to pcp_list in '__rmqueue_pcplist()` only happen once, pcp->count changed to 200 from 0 for example.
The later calls to __rmqueue_pcplist() will drop pcp->count by step 1 for each call, For example, pcp->count: 199->198->197->196...,
However it stops calling __rmqueue_pcplist after a few times before pcp->count is dropped to 0.
In the normal/good case, we also noticed that __rmqueue_pcplist() dropped pcp->count to 0.
Here is the __rmqueue_pcplist() call trace.
CPU: 1 PID: 3615 Comm: consume_std_pag Not tainted 6.10.0-rc2+ #147 Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org 04/01/2014 Call Trace: <TASK> dump_stack_lvl+0x64/0x80 __rmqueue_pcplist+0xd55/0xdf0 get_page_from_freelist+0x2a1/0x1770 __alloc_pages_noprof+0x1a0/0x380 alloc_pages_mpol_noprof+0xe3/0x1f0 vma_alloc_folio_noprof+0x5c/0xb0 folio_prealloc+0x21/0x80 do_pte_missing+0x695/0xa20 ? __pte_offset_map+0x1b/0x180 __handle_mm_fault+0x65f/0xc10 ? kmem_cache_free+0x370/0x410 handle_mm_fault+0x128/0x360 do_user_addr_fault+0x309/0x810 exc_page_fault+0x7e/0x180 asm_exc_page_fault+0x26/0x30 RIP: 0033:0x7f3d8729028a
In the meantime, decay_pcp_high() will be called to drop the pcp->count periodically.
[ 145.117256] decay_pcp_high+0x68/0x90 [ 145.117256] refresh_cpu_vm_stats+0x149/0x2a0 [ 145.117256] vmstat_update+0x13/0x50 [ 145.117256] process_scheduled_works+0xa6/0x420 [ 145.117256] worker_thread+0x117/0x270 [ 145.117256] ? __pfx_worker_thread+0x10/0x10 [ 145.117256] kthread+0xe5/0x120 [ 145.117256] ? __pfx_kthread+0x10/0x10 [ 145.117256] ret_from_fork+0x34/0x40 [ 145.117256] ? __pfx_kthread+0x10/0x10 [ 145.117256] ret_from_fork_asm+0x1a/0x30
But decay_pcp_high() will stop dropping pcp->count after a few moment later. IOW decay_pcp_high() will be called periodically though, it doesn't drop pcp->count. A piece of pcp content shows as below:
...
count = 7,
high = 7,
high_min = 0,
high_max = 0,
batch = 1,
flags = 0 '\000',
alloc_factor = 3 '\003',
expire = 0 '\000',
free_count = 0,
lists = {{
...
2244 * Called from the vmstat counter updater to decay the PCP high. 2245 * Return whether there are addition works to do. 2246 */
2247 int decay_pcp_high(struct zone *zone, struct per_cpu_pages *pcp) 2248 {
2249 int high_min, to_drain, batch;
2250 int todo = 0;
2251
2252 high_min = READ_ONCE(pcp->high_min); 2253 batch = READ_ONCE(pcp->batch); 2254 /* 2255 * Decrease pcp->high periodically to try to free possible 2256 * idle PCP pages. And, avoid to free too many pages to 2257 * control latency. This caps pcp->high decrement too. 2258 */
2259 if (pcp->high > high_min) {
2260 pcp->high = max3(pcp->count - (batch << CONFIG_PCP_BATCH_SCALE_MAX),
2261 pcp->high - (pcp->high >> 3), high_min);
2262 if (pcp->high > high_min)
2263 todo++;
2264 }
2265
2266 to_drain = pcp->count - pcp->high; // to_drain will be 0(when count == high),
// so no any pages can be drop from pcp_list.
2267 if (to_drain > 0) {
2268 spin_lock(&pcp->lock);
2269 free_pcppages_bulk(zone, to_drain, pcp, 0);
2270 spin_unlock(&pcp->lock);
2271 todo++;2272 }
2273
2274 if (mutex_is_locked(&pcp_batch_high_lock) && pcp->high_max == 0 && to_drain > 0 && pcp->count >= 0)
2275 pr_info("lizhijian:%s,%d: cpu%d, to_drain %d, new %d\n", __func__, __LINE__, smp_processor_id(), to_drain, pcp->count);
2277 return todo;
2278 }
I'm wondering if we can fix it in decay_pcp_high(), let it drop pcp->count to 0 when zone_pcp_disable()
The following logs show the pcp->count(*new* is pcp->count in the end of the function) in the bad case.
=====================
Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a0159, old 71, new 70, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015a, old 70, new 69, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015b, old 69, new 68, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015c, old 68, new 67, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015d, old 67, new 66, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015e, old 66, new 65, add 0, drop 1 Jul 11 18:34:14 linux kernel: lizhijian:__rmqueue_pcplist,2945: cpu1, pfn 6a015f, old 65, new 64, add 0, drop 1 ... Jul 11 18:34:18 linux kernel: lizhijian: offline_pages,2087: cpu0: [6a0000-6a8000] get trouble: pcplist[1]: 0->63, batch 1, high 154
...
Jul 11 18:34:25 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 7, new 56 Jul 11 18:34:26 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 7, new 49 Jul 11 18:34:27 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 6, new 43 ... Jul 11 18:34:40 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 1, new 10 Jul 11 18:34:41 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 1, new 9 Jul 11 18:34:42 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 1, new 8 Jul 11 18:34:43 linux kernel: lizhijian:decay_pcp_high,2275: cpu1, to_drain 1, new 7
=====================
Thanks
Zhijian
Quoted 12 lines
Thanks
ZhijianQuoted 6 lines
--
Cheers,David / dhildenb
Quoted 19 lines · Zhijian Li (Fujitsu)
CPU0 CPU1 ----------- ---------// erase
pcp_listzone_pcp_disable//pcp->count= 0
lru_cache_diable()__rmqueue_pcplist()// re-add pages topcp_lsit
__rmqueue_pcplist()// drop pages frompcp_list
decay_pcp_high()// drop pages frompcp_list
loop { ...
__rmqueue_pcplist()// drop pages frompcp_list,
// it will be only called a few times during the loop
scan_movable_pages()...
migration_pages()decay_pcp_high()// drop pages frompcp_list, it will be called by
// a worker periodically during the loop// wait
pcp_listto be empty
} while (test_pages_isolated())
Quoted 8 lines
And we noticed that re-add pages to
pcp_listin '__rmqueue_pcplist()` only happen once,pcp->countchanged to 200 from 0 for example.The later calls to
__rmqueue_pcplist()will droppcp->countby step 1 for each call, For example,pcp->count: 199->198->197->196...,
However it stops calling__rmqueue_pcplistafter a few times beforepcp->countis dropped to 0.In the normal/good case, we also noticed that
__rmqueue_pcplist()droppedpcp->countto 0.
I doubt all these(after calling zone_pcp_disable())
1. __rmqueue_pcplist() should re-add pages to pcp_list
2. __rmqueue_pcplist() should drop pcp->count till 0 if 1 is true
3. decay_pcp_high() should drop pcp->count till 0, its name and comments don't indicate this.