热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux操作系统学习笔记(三)内核初始化

前言前文分析到Linux内核正式启动,完成了实模式到保护模式的切换,并做好了各种准备工作。下来就要看开始内核初始化工作了,源码位置位于in

前言

  前文分析到Linux内核正式启动,完成了实模式到保护模式的切换,并做好了各种准备工作。下来就要看开始内核初始化工作了,源码位置位于init/main.c中的start_kernel(),源码如附录所示。这包括了一系列重要的初始化工作,本文会介绍其中一部分较为重要的,但是详细的介绍依然会留在后文各个模块的源码学习中单独进行。本文的目的在于承接上文给出一个从内核启动到各个模块开始运转的过程介绍,而不是详细的各部分内容介绍。

  • 创建0号进程:INIT_TASK(init_task)

  • 异常处理类中断服务程序挂接:trap_init()

  • 内存初始化:mm_init()

  • 调度器初始化sched_init()

  • 剩余初始化:rest_init()


0号进程的创建

  start_kernel()上来就会运行 set_task_stack_end_magic(&init_task)创建初始进程。init_task的定义是 struct task_struct init_task = INIT_TASK(init_task)。它是系统创建的第一个进程,我们称为 0 号进程。这是唯一一个没有通过 fork 或者 kernel_thread产生的进程,是进程列表的第一个

  如下所示为init_task的定义,这里只节选了部分,采用了gcc的结构体初始化方式为其进行了直接赋值生成。

/** Set up the first task table, touch at your own risk!. Base=0,* limit=0x1fffff (=2MB)*/
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK__init_task_data
#endif
= {.......state = 0,.stack = init_stack,.usage = REFCOUNT_INIT(2),.flags = PF_KTHREAD,.prio = MAX_PRIO - 20,.static_prio = MAX_PRIO - 20,.normal_prio = MAX_PRIO - 20,.policy = SCHED_NORMAL,.cpus_ptr = &init_task.cpus_mask,.cpus_mask = CPU_MASK_ALL,.nr_cpus_allowed= NR_CPUS,.mm = NULL,.active_mm = &init_mm,.......thread_pid = &init_struct_pid,.thread_group = LIST_HEAD_INIT(init_task.thread_group),.thread_node = LIST_HEAD_INIT(init_signals.thread_head),......
};
EXPORT_SYMBOL(init_task);

  而 set_task_stack_end_magic(&init_task)函数的源码如下,主要是通过end_of_stack()获取栈边界地址,然后把栈底地址设置为STACK_END_MAGIC,作为栈溢出的标记。每个进程创建的时候,系统会为这个进程创建2个页大小的内核栈。

void set_task_stack_end_magic(struct task_struct *tsk)
{unsigned long *stackend;stackend = end_of_stack(tsk);*stackend = STACK_END_MAGIC; /* for overflow detection */
}

  init_task是静态定义的一个进程,也就是说当内核被放入内存时,它就已经存在,它没有自己的用户空间,一直处于内核空间中运行,并且也只处于内核空间运行。0号进程用于包括内存、页表、必要数据结构、信号、调度器、硬件设备等的初始化。当它执行到最后(剩余初始化)时,将start_kernel中所有的初始化执行完成后,会在内核中启动一个kernel_init内核线程和一个kthreadd内核线程,kernel_init内核线程执行到最后会通过execve系统调用执行转变为我们所熟悉的init进程,而kthreadd内核线程是内核用于管理调度其他的内核线程的守护线程。在最后init_task将变成一个idle进程,用于在CPU没有进程运行时运行它,它在此时仅仅用于空转。

中断初始化

  由代码可见,trap_init()设置了很多的中断门(Interrupt Gate),用于处理各种中断,如系统调用的中断门set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)

void trap_init(void)
{int i;//设置系统的硬件中断 中断位于kernel/asm.s 或 system_call.sset_trap_gate(0,÷_error);//0中断&#xff0c;位于/kernel/asm.s 19行set_trap_gate(1,&debug);set_trap_gate(2,&nmi);set_system_gate(3,&int3); /* int3-5 can be called from all */set_system_gate(4,&overflow);set_system_gate(5,&bounds);set_trap_gate(6,&invalid_op);set_trap_gate(7,&device_not_available);set_trap_gate(8,&double_fault);set_trap_gate(9,&coprocessor_segment_overrun);set_trap_gate(10,&invalid_TSS);set_trap_gate(11,&segment_not_present);set_trap_gate(12,&stack_segment);set_trap_gate(13,&general_protection);set_trap_gate(14,&page_fault);set_trap_gate(15,&reserved);set_trap_gate(16,&coprocessor_error);for (i&#61;17;i<48;i&#43;&#43;)set_trap_gate(i,&reserved);set_trap_gate(45,&irq13);outb_p(inb_p(0x21)&0xfb,0x21);outb(inb_p(0xA1)&0xdf,0xA1);set_trap_gate(39,¶llel_interrupt);
}

内存初始化

  内存相关的初始化内容放在mm_init()中进行&#xff0c;代码如下所示

// init/main.c
/** Set up kernel memory allocators*/
static void __init mm_init(void)
{/** page_ext requires contiguous pages,* bigger than MAX_ORDER unless SPARSEMEM.*/page_ext_init_flatmem();mem_init();kmem_cache_init();pgtable_init();vmalloc_init();ioremap_huge_init();/* Should be run before the first non-init thread is created */init_espfix_bsp();/* Should be run after espfix64 is set up. */pti_init();
}

  调用的函数功能基本如名字所示&#xff0c;主要进行了以下初始化设置&#xff1a;

  • page_ext_init_flatmem()和cgroup的初始化相关&#xff0c;该部分是docker技术的核心部分
  • mem_init()初始化内存管理的伙伴系统
  • kmem_cache_init()完成内核slub内存分配体系的初始化&#xff0c;相关的还有buffer_init
  • pgtable_init()完成页表初始化&#xff0c;包括页表锁ptlock_init()
  • vmalloc_init()完成vmalloc的初始化
  • ioremap_huge_init() ioremap实现I/O内存资源由物理地址映射到虚拟地址空间&#xff0c;此处为其功能的初始化
  • init_espfix_bsp()pti_init()完成PTI&#xff08;page table isolation&#xff09;的初始化

  此处不展开说明这些函数&#xff0c;留待后面内存管理部分详细分析各个部分。

调度器初始化

  调度器初始化通过sched_init()完成&#xff0c;其主要工作包括

  • 对相关数据结构分配内存&#xff1a;如初始化waitqueues数组&#xff0c;根据调度方式FAIR/RT设置alloc_size&#xff0c;调用kzalloc分配空间
  • 初始化root_task_group&#xff1a;根据FAIR/RT的不同&#xff0c;将kzalloc分配的空间用于其初始化&#xff0c;主要结构task_group包含以下几个重要组成部分&#xff1a;se, rt_se, cfs_rq 以及 rt_rq。其中cfs_rqrt_rq表示run queue&#xff0c;即一种特殊的per-cpu结构体用于内核调度器存储激活的线程。
  • 调用for_each_possible_cpu()初始化每个possibleCPU&#xff08;存储于cpu_possible_mask为图中&#xff09;的runqueue队列(包括其中的cfs队列和实时进程队列)&#xff0c;rq结构体是调度进程的基本数据结构&#xff0c;调度器用rq决定下一个将要被调度的进程。详细介绍会在调度一节进行。
  • 调用set_load_weight(&init_task)&#xff0c;将init_task进程转变为idle进程

需要说明的是init_task在这里会被转变为idle进程&#xff0c;但是它还会继续执行初始化工作&#xff0c;相当于这里只是给init_task挂个idle进程的名号&#xff0c;它其实还是init_task进程&#xff0c;只有到最后init_task进程开启了kernel_initkthreadd进程之后&#xff0c;才转变为真正意义上的idle进程。

剩余初始化

  rest_init是非常重要的一步&#xff0c;主要包括了区分内核态和用户态、初始化1号进程和初始化2号进程。

内核态和用户态

  在运行用户进程之前&#xff0c;尚需要完成一件事&#xff1a;区分内核态和用户态。x86 提供了分层的权限机制&#xff0c;把区域分成了四个 Ring&#xff0c;越往里权限越高&#xff0c;越往外权限越低。操作系统很好地利用了这个机制&#xff0c;将能够访问关键资源的代码放在 Ring0&#xff0c;我们称为内核态&#xff08;Kernel Mode&#xff09;&#xff1b;将普通的程序代码放在 Ring3&#xff0c;我们称为用户态&#xff08;User Mode&#xff09;。

img

初始化1号进程

  rest_init() 的一大工作是&#xff0c;用 kernel_thread(kernel_init, NULL, CLONE_FS)创建第二个进程&#xff0c;这个是 1 号进程。1 号进程对于操作系统来讲&#xff0c;有“划时代”的意义&#xff0c;因为它将运行一个用户进程&#xff0c;并从此开始形成用户态进程树。这里主要需要分析的是如何完成从内核态到用户态切换的过程。kernel_thread()代码如下所示&#xff0c;可见其中最主要的是第一个参数指针函数fn决定了栈中的内容&#xff0c;根据fn的不同将生成1号进程和后面的2号进程。

/** Create a kernel thread.*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{struct kernel_clone_args args &#61; {.flags &#61; ((flags | CLONE_VM | CLONE_UNTRACED) & ~CSIGNAL),.exit_signal &#61; (flags & CSIGNAL),.stack &#61; (unsigned long)fn,.stack_size &#61; (unsigned long)arg,};return _do_fork(&args);
}

  kernel_thread() 的参数是一个函数 kernel_init()&#xff0c;核心代码如下&#xff1a;

if (ramdisk_execute_command)
{ ret &#61; run_init_process(ramdisk_execute_command);......
}
......
if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0;

  这就说明&#xff0c;1 号进程运行的是一个文件。如果我们打开 run_init_process() 函数&#xff0c;会发现它调用的是 do_execve()

static int run_init_process(const char *init_filename)
{ argv_init[0] &#61; init_filename; return do_execve(getname_kernel(init_filename), (const char __user *const __user *)argv_init, (const char __user *const __user *)envp_init);
}

  接着会进行一系列的调用&#xff1a;do_execve->do_execveat_common->exec_binprm->search_binary_handler&#xff0c;这里search_binary_handler()主要是加载ELF文件&#xff08;Executable and Linkable Format&#xff0c;可执行与可链接格式&#xff09;&#xff0c;代码如下

int search_binary_handler(struct linux_binprm *bprm)
{ ...... struct linux_binfmt *fmt; ...... retval &#61; fmt->load_binary(bprm); ......
}

  load_binary先调用load_elf_binary&#xff0c;最后调用start_thread

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{set_user_gs(regs, 0);regs->fs &#61; 0;regs->ds &#61; __USER_DS;regs->es &#61; __USER_DS;regs->ss &#61; __USER_DS;regs->cs &#61; __USER_CS;regs->ip &#61; new_ip;regs->sp &#61; new_sp;regs->flags &#61; X86_EFLAGS_IF;force_iret();
}
EXPORT_SYMBOL_GPL(start_thread);

  这个结构就是在系统调用的时候&#xff0c;内核中保存用户态运行上下文的&#xff0c;里面将用户态的代码段 CS 设置为 __USER_CS&#xff0c;将用户态的数据段 DS 设置为 __USER_DS&#xff0c;以及指令指针寄存器 IP、栈指针寄存器 SP。这里相当于补上了原来系统调用里&#xff0c;保存寄存器的一个步骤。最后的 iret 是干什么的呢&#xff1f;它是用于从系统调用中返回。这个时候会恢复寄存器。从哪里恢复呢&#xff1f;按说是从进入系统调用的时候&#xff0c;保存的寄存器里面拿出。好在上面的函数补上了寄存器。CS 和指令指针寄存器 IP 恢复了&#xff0c;指向用户态下一个要执行的语句。DS 和函数栈指针 SP 也被恢复了&#xff0c;指向用户态函数栈的栈顶。所以&#xff0c;下一条指令&#xff0c;就从用户态开始运行了。

  经过上述过程&#xff0c;我们完成了从内核态切换到用户态。而此时代码其实还在运行 kernel_init函数&#xff0c;会调用

if (!ramdisk_execute_command)ramdisk_execute_command &#61; "/init";

  结合上面的init程序&#xff0c;这里出现了第二个init。这是有其存在的必要性的&#xff1a;上文提到的 init 程序是在文件系统上的&#xff0c;文件系统一定是在一个存储设备上的&#xff0c;例如硬盘。Linux 访问存储设备&#xff0c;要有驱动才能访问。如果存储系统数目很有限&#xff0c;那驱动可以直接放到内核里面&#xff0c;反正前面我们加载过内核到内存里了&#xff0c;现在可以直接对存储系统进行访问。但是存储系统越来越多了&#xff0c;如果所有市面上的存储系统的驱动都默认放进内核&#xff0c;内核就太大了。这该怎么办呢&#xff1f;

  我们只好先弄一个基于内存的文件系统。内存访问是不需要驱动的&#xff0c;这个就是 ramdisk。这个时候&#xff0c;ramdisk 是根文件系统。然后&#xff0c;我们开始运行 ramdisk 上的 /init。等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动&#xff0c;有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统&#xff0c;ramdisk 上的 /init 会启动文件系统上的 init。接下来就是各种系统的初始化。启动系统的服务&#xff0c;启动控制台&#xff0c;用户就可以登录进来了。

初始化2号进程

  rest_init 另一大事情就是创建第三个进程&#xff0c;就是 2 号进程。kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)又一次使用 kernel_thread 函数创建进程。这里需要指出一点&#xff0c;函数名 thread 可以翻译成“线程”&#xff0c;这也是操作系统很重要的一个概念。从内核态来看&#xff0c;无论是进程&#xff0c;还是线程&#xff0c;我们都可以统称为任务&#xff08;Task&#xff09;&#xff0c;都使用相同的数据结构&#xff0c;平放在同一个链表中。这里的函数kthreadd&#xff0c;负责所有内核态的线程的调度和管理&#xff0c;是内核态所有线程运行的祖先。

  kthreadd&#xff0c;即2号进程&#xff0c;用于内核态线程的管理&#xff0c;是一个守护线程。其源码如下所示&#xff0c;运行流程包括

  • 初始化了task结构&#xff0c;并将该线程设置为允许任意CPU运行。
  • 进入循环&#xff0c;将线程状态设置为TASK_INTERRUPTIBLE&#xff0c;如果当前kthread_create_list为空&#xff0c;没有要创建的线程&#xff0c;则执行schedule()让出CPU资源。
  • 如果需要创建&#xff0c;则设置为TASK_RUNNING状态&#xff0c;加上锁spin_lock&#xff0c;从链表中取得kthread_create_info 结构的地址&#xff0c;在上文中已经完成插入操作(将kthread_create_info结构中的 list 成员加到链表中&#xff0c;此时根据成员 list 的偏移获得 create)
  • 调用create_kthread(create)完成线程的创建

int kthreadd(void *unused)
{struct task_struct *tsk &#61; current;/* Setup a clean context for our children to inherit. */set_task_comm(tsk, "kthreadd");ignore_signals(tsk);set_cpus_allowed_ptr(tsk, cpu_all_mask);set_mems_allowed(node_states[N_MEMORY]);current->flags |&#61; PF_NOFREEZE;cgroup_init_kthreadd();for (;;) {set_current_state(TASK_INTERRUPTIBLE);if (list_empty(&kthread_create_list))schedule();__set_current_state(TASK_RUNNING);spin_lock(&kthread_create_lock);while (!list_empty(&kthread_create_list)) {struct kthread_create_info *create;create &#61; list_entry(kthread_create_list.next,struct kthread_create_info, list);list_del_init(&create->list);spin_unlock(&kthread_create_lock);create_kthread(create);spin_lock(&kthread_create_lock);}spin_unlock(&kthread_create_lock);}return 0;
}

  而create_kthread(create)函数做了一件让人意外的事情&#xff1a;调用了kernel_thread()&#xff0c;所以又回到了创建1号进程和2号进程的函数上&#xff0c;这次的回调函数为kthread&#xff0c;该函数才会真正意义上分配内存、初始化一个新的内核线程。

static void create_kthread(struct kthread_create_info *create)
{int pid;#ifdef CONFIG_NUMAcurrent->pref_node_fork &#61; create->node;
#endif/* We want our own signal handler (we take no signals by default). */pid &#61; kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);if (pid < 0) {/* If user was SIGKILLed, I release the structure. */struct completion *done &#61; xchg(&create->done, NULL);if (!done) {kfree(create);return;}create->result &#61; ERR_PTR(pid);complete(done);}
}

  下面是kthread的源码&#xff0c;这里有个很重要的地方&#xff1a;新创建的线程由于执行了 schedule() 调度&#xff0c;此时并没有执行&#xff0c;直到我们使用wake_up_process(p)唤醒新创建的线程。线程被唤醒后, 会接着执行最后一段threadfn(data)

static int kthread(void *_create)
{/* Copy data: it&#39;s on kthread&#39;s stack */struct kthread_create_info *create &#61; _create;int (*threadfn)(void *data) &#61; create->threadfn;void *data &#61; create->data;struct completion *done;struct kthread *self;int ret;self &#61; kzalloc(sizeof(*self), GFP_KERNEL);set_kthread_struct(self);/* If user was SIGKILLed, I release the structure. */done &#61; xchg(&create->done, NULL);if (!done) {kfree(create);do_exit(-EINTR);}if (!self) {create->result &#61; ERR_PTR(-ENOMEM);complete(done);do_exit(-ENOMEM);}self->data &#61; data;init_completion(&self->exited);init_completion(&self->parked);current->vfork_done &#61; &self->exited;/* OK, tell user we&#39;re spawned, wait for stop or wakeup */__set_current_state(TASK_UNINTERRUPTIBLE);create->result &#61; current;/** Thread is going to call schedule(), do not preempt it,* or the creator may spend more time in wait_task_inactive().*/preempt_disable();complete(done);schedule_preempt_disabled();preempt_enable();ret &#61; -EINTR;if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {cgroup_kthread_ready();__kthread_parkme(self);ret &#61; threadfn(data);}do_exit(ret);
}

  由此&#xff0c;我们可以总结一下第2号进程的工作流程&#xff1a;

  • 第2号进程kthreadd进程由第0号进程通过kernel_thread()创建&#xff0c;并始终运行在内核空间, 负责所有内核线程的调度和管理
  • 第2号进程会循环检测kthread_create_list全局链表, 当我们调用kernel_thread创建内核线程时&#xff0c;新线程会被加入到此链表中&#xff0c;因此所有的内核线程都是直接或者间接的以kthreadd为父进程
  • 检测到新线程创建&#xff0c;则调用kernel_thread()创建线程&#xff0c;其回调为kthread
  • kthread在创建完后调用schedule()让出CPU资源&#xff0c;而不是直接运行。等待收到wake_up_process(p)的唤醒后再继续执行threadfn(data)

因此

  • 任何一个内核线程入口都是 kthread()

  • 通过kthread_create()创建的内核线程不会立刻运行&#xff0c;需要手工 wake up.

  • 通过kthread_create() 创建的内核线程有可能不会执行相应线程函数threadfn而直接退出

  回到rest_init()&#xff0c;当完成了1号2号进程的创建后&#xff0c;我们将0号进程真正归位idle进程&#xff0c;结束rest_init()&#xff0c;也正事结束了start_kernel()函数&#xff0c;由此&#xff0c;内核初始化全部完成。

总结

  本文介绍了内核初始化的几个重要部分&#xff0c;其实还有很多初始化没有介绍&#xff0c;如cgroup初始化、虚拟文件系统初始化、radix树初始化、rcu初始化、计时器和时间初始化、架构初始化等等&#xff0c;这些会在后面有针对性的单独介绍。

源码资料

[1] init/main.c

参考资料

[1] Linux-insides

[2] 深入理解Linux内核源码

[3] Linux内核设计的艺术

[4] 极客时间 趣谈Linux操作系统


推荐阅读
  • 本文探讨了如何通过编程手段在Linux系统中禁用硬件预取功能。基于Intel® Core™微架构的应用性能优化需求,文章详细介绍了相关配置方法和代码实现,旨在帮助开发人员有效控制硬件预取行为,提升应用程序的运行效率。 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • MATLAB字典学习工具箱SPAMS:稀疏与字典学习的详细介绍、配置及应用实例
    SPAMS(Sparse Modeling Software)是一个强大的开源优化工具箱,专为解决多种稀疏估计问题而设计。该工具箱基于MATLAB,提供了丰富的算法和函数,适用于字典学习、信号处理和机器学习等领域。本文将详细介绍SPAMS的配置方法、核心功能及其在实际应用中的典型案例,帮助用户更好地理解和使用这一工具箱。 ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • 手指触控|Android电容屏幕驱动调试指南
    手指触控|Android电容屏幕驱动调试指南 ... [详细]
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 在腾讯云服务器上部署Nginx的详细指南中,首先需要确保安装必要的依赖包。如果这些依赖包已安装,可直接跳过此步骤。具体命令包括 `yum -y install gcc gcc-c++ wget net-tools pcre-devel zlib-devel`。接下来,本文将详细介绍如何下载、编译和配置Nginx,以确保其在腾讯云服务器上顺利运行。此外,还将提供一些优化建议,帮助用户提升Nginx的性能和安全性。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 本文探讨了如何利用Java代码获取当前本地操作系统中正在运行的进程列表及其详细信息。通过引入必要的包和类,开发者可以轻松地实现这一功能,为系统监控和管理提供有力支持。示例代码展示了具体实现方法,适用于需要了解系统进程状态的开发人员。 ... [详细]
  • 本文详细介绍了在Linux系统上编译安装MySQL 5.5源码的步骤。首先,通过Yum安装必要的依赖软件包,如GCC、GCC-C++等,确保编译环境的完备。接着,下载并解压MySQL 5.5的源码包,配置编译选项,进行编译和安装。最后,完成安装后,进行基本的配置和启动测试,确保MySQL服务正常运行。 ... [详细]
  • 具备括号和分数功能的高级四则运算计算器
    本研究基于C语言开发了一款支持括号和分数运算的高级四则运算计算器。该计算器通过模拟手算过程,对每个运算符进行优先级标记,并按优先级从高到低依次执行计算。其中,加减运算的优先级最低,为0。此外,该计算器还支持复杂的分数运算,能够处理包含括号的表达式,提高了计算的准确性和灵活性。 ... [详细]
  • 本文探讨了 Kafka 集群的高效部署与优化策略。首先介绍了 Kafka 的下载与安装步骤,包括从官方网站获取最新版本的压缩包并进行解压。随后详细讨论了集群配置的最佳实践,涵盖节点选择、网络优化和性能调优等方面,旨在提升系统的稳定性和处理能力。此外,还提供了常见的故障排查方法和监控方案,帮助运维人员更好地管理和维护 Kafka 集群。 ... [详细]
  • 求助:在CentOS 5.8系统上安装PECL扩展遇到问题
    在 CentOS 5.8 系统上尝试安装 APC 扩展时遇到了问题,具体表现为 PECL 工具无法正常工作。为了确保顺利安装,需要解决 PECL 的相关依赖和配置问题。建议检查 PHP 和 PECL 的版本兼容性,并确保所有必要的库和开发工具已正确安装。此外,可以尝试手动下载 APC 扩展的源代码并进行编译安装,以绕过 PECL 工具的限制。 ... [详细]
  • 在Ubuntu系统中配置Python环境变量是确保项目顺利运行的关键步骤。本文介绍了如何将Windows上的Django项目迁移到Ubuntu,并解决因虚拟环境导致的模块缺失问题。通过详细的操作指南,帮助读者正确配置虚拟环境,确保所有第三方库都能被正确识别和使用。此外,还提供了一些实用的技巧,如如何检查环境变量配置是否正确,以及如何在多个虚拟环境之间切换。 ... [详细]
author-avatar
书友36110188
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有