精华内容
下载资源
问答
  • C++/C++11中命名空间(namespace)的使用

    万次阅读 多人点赞 2017-11-19 18:54:40
    C++/C++11中命名空间(namespace)的使用

    大型程序往往会使用多个独立开发的库,这些库又会定义大量的全局名字,如类、函数和模板等。当应用程序用到多个供应商提供的库时,不可避免地会发生某些名字相互冲突的情况。多个库将名字放置在全局命名空间中将引发命名空间污染(namespace pollution)。

    传统上,程序员通过将其定义的全局实体名字设得很长来避免命名空间污染问题,这样的名字中通常包含表示名字所属库的前缀部分。这种解决方案显然不太理想:对于程序员来说,书写和阅读这么长的名字费时费力且过于繁琐。

    命名空间(namespace)为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者以及用户可以避免全局名字固有的限制。

    命名空间定义:一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其它命名空间。命名空间结束后无须分号,这一点与块类似。和其它名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间既可以定义在全局作用域内,也可以定义在其它命名空间中,但是不能定义在函数或类的内部。命名空间作用域后面无须分号。

    每个命名空间都是一个作用域:和其它作用域类似,命名空间中的每个名字都必须表示该空间内的唯一实体。因为不同命名空间的作用域不同,所以在不同命名空间内可以有相同名字的成员。

    定义在某个命名空间中的名字可以被该命名空间内的其它成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所用的名字属于哪个命名空间。

    命名空间可以是不连续的:命名空间可以定义在几个不同的部分,这一点与其它作用域不太一样。命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似于我们管理自定义类及函数的方式:命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。命名空间成员的定义部分则置于另外的源文件中。

    在程序中某些实体只能定义一次:如非内联函数、静态数据成员、变量等,命名空间中定义的名字也需要满足这一要求。这种接口和实现分离的机制确保我们所需的函数和其它名字只定义一次,而只要是用到这些实体的地方都能看到对于实体名字的声明。定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型(或关联类型构成的集合)。

    #include 应该出现在打开命名空间的操作之前。在通常情况下,我们不把#include放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。

    定义命名空间成员:假定作用域中存在合适的声明语句,则命名空间中的代码可以使用同一命名空间定义的名字的简写形式。也可以在命名空间定义的外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。命名空间之外定义的成员必须使用含有前缀的名字。和定义在类外部的类成员一样,一旦看到含有完整前缀的名字,我们就可以确定该名字位于命名空间的作用域内。尽管命名空间的成员可以定义在命名空间外部,但是这样的定义必须出现在所属命名空间的外层空间中。

    模板特例化:模板特例化必须定义在原始模板所属的命名空间中。和其它命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了。

    全局命名空间:全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间(global namespace)中。全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字被隐式地添加到全局命名空间中。

    作用域运算符(::)同样可以用于全局作用域的成员,因为全局作用域是隐式的,所以它并没有名字。

    嵌套的命名空间:是指定义在其它命名空间中的命名空间。嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符。

    内联命名空间:C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace)。和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。定义内联命名空间的方式是在关键字namespace前添加关键字inline。关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间。

    未命名的命名空间(unnamed namespace):是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。

    一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。和其它命名空间不同,未命名的命名空间仅在特定的文件内有效,其作用范围不会横跨多个不同的文件。

    定义在未命名的命名空间中的名字可以直接使用,毕竟我们找不到什么命名空间的名字来限定它们;同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。

    未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。其它情况下,未命名的命名空间中的成员都属于正确的程序实体。和所有命名空间类似,一个未命名的命名空间也能嵌套在其它命名空间当中。此时,未命名的命名空间中的成员可以通过外层命名空间的名字来访问。

    未命名的命名空间取代文件中的静态声明:在标准C++引入命名空间的概念之前,程序需要将名字声明成static的以使得其对于整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的。在C语言中,声明为static的全局实体在其所在的文件外不可见。在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

    使用命名空间成员:命名空间的别名(namespace alias)使得我们可以为命名空间的名字设定一个短得多的同义词。命名空间的别名声明以关键字namespace开始,后面是别名所用的名字、=符号、命名空间原来的名字以及一个分号。不能在命名空间还没有定义前就声明别名,否则将产生错误。命名空间的别名也可以指向一个嵌套的命名空间。一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。

    using声明:一条using声明(usingdeclaration)语句一次只引入命名空间的一个成员。它使得我们可以清楚地知道程序中所用的到底是哪个名字。

    using声明引入的名字遵守与过去一样的作用域规则:它的有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。未加限定的名字只能在using声明所在的作用域以及其内层作用域中使用。在有效作用域结束后,我们就必须使用完整的经过限定的名字了。

    一条using声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类的作用域中。在类的作用域中,这样的声明语句只能指向基类成员。

    using指示(usingdirective):和using声明类似的地方是,我们可以使用命名空间名字的简写形式;和using声明不同的地方是,我们无法控制哪些名字是可见的,因为所有名字都是可见的。using指示以关键字using开始,后面是关键字namespace以及命名空间的名字。如果这里所用的名字不是一个已经定义好的命名空间的名字,则程序将发生错误。using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中。

    using指示使得某个特定的命名空间中的所有名字都可见,这样我们就无须再为它们添加任何前缀限定符了。简写的名字从using指示开始,一直到using指示所在的作用域结束都能使用。如果我们提供了一个对std等命名空间的using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题。

    using指示与作用域:using指示引入的名字的作用域远比using生命引入的名字的作用域复杂。using声明的名字的作用域与using声明语句本身的作用域一致,从效果看就好像using声明语句为命名空间的成员在当前作用域内创建了一个别名一样。using指示所做的绝非声明别名这么简单。相反,它具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。

    using声明和using指示在作用域上的区别直接决定了它们工作方式的不同。对应using声明来说,我们只是简单地令名字在局部作用域内有效。相反,using指示是令整个命名空间的所有内容变得有效。通常情况下,命名空间中会含有一些不能出现在局部作用域中的定义,因此,using指示一般被看作是出现在最近的外层作用域中。

    当命名空间被注入到它的外层作用域之后,很有可能该命名空间中定义的名字会与其外层作用域中的成员冲突。这种冲突是允许存在的,但是要想使用冲突的名字,我们就必须明确指出名字的版本。

    头文件与using声明或指示:头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。

    避免using指示:using指示一次性注入某个命名空间的所有名字,这种用法看似简单实则充满了风险:只使用一条语句就突然将命名空间中所有成员的名字变得可见了。如果应用程序使用了多个不同的库,而这些库中的名字通过using指示变得可见,则全局命名空间污染的问题将重新出现。而且,当引入库的新版本后,正在工作的程序很可能会编译失败。如果新版本引入了一个与应用程序正在使用的名字冲突的名字,就会出现这个问题。另一个风险是由using指示引发的二义性错误只有在使用了冲突名字的地方才能被发现。这种延后的检测意味着可能在特定库引入很久之后才爆发冲突。直到程序开始使用该库的新部分后,之前一直未被检测到的错误才会出现。

    相比于使用using指示,在程序中对命名空间的每个成员分别使用using声明效果更好,这么做可以减少注入到命名空间中的名字数量。using声明引起的二义性问题在声明处就能发现,无须等到使用名字的地方,这显然对检测并修改错误大有益处。

    using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用using指示。

    类、命名空间与作用域:对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。对于位于命名空间中的类来说,常规的查找规则仍然适用:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找。可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域。

    实参相关的查找与类类型形参:对于命名空间中名字的隐藏规则来说有一个重要的例外。这个例外是,当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。查找规则的这个例外运行概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。

    using声明语句声明的是一个名字,而非一个特定的函数。当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口。库的作者为某项任务提供了好几个不同的函数,允许用户选择性地忽略重载函数中的一部分但不是全部有可能导致意想不到的程序行为。

    一个using声明引入的函数将重载该声明语句所属作用域中已有的其它同名函数。如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域中已经有一个函数或新引入的函数同名且形参列表相同,则该using声明将引发错误。除此之外,using声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模。

    using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中。与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数将不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分。

    在C++语言中,命名空间是一种实体(entity),使用namespace来声明,并使用{}来界定命名空间体(namespacebody)。和C语言的全局作用域兼容,C++具有全局命名空间作用域,对应的命名空间是全局命名空间。全局命名空间不需要声明。使用时,可以用前缀为::的qualified-id显示限定全局命名空间作用域中的名称。

    命名空间可以在另一命名空间之中嵌套声明;但不能声明在类和代码块之中。在命名空间中声明的名称,默认具有外部链接属性(除非声明的是const对象,它默认是具有内部链接属性)。

    按照是否有名字,可分为有名字的命名空间和匿名命名空间。匿名命名空间中的名字具有文件作用域。这些名字在本编译单元中可以直接使用;也可以用前缀为::的qualified-id显示限定后使用。匿名命名空间中的名字具有内部链接属性。

    命名空间的成员,是在命名空间体的花括号内声明了的名称。可以在命名空间体之外,给出命名空间成员的定义。即命名空间的成员声明与定义可以分开。命名空间内的名字,只能有一次定义,但可以多次声明。嵌套的子命名空间必须定义在上层命名空间体之内。禁止把子命名空间的声明与定义分开。不能以”命名空间名::成员名;”方式,在命名空间体之外为命名空间添加新成员。必须在命名空间体之中添加新成员的声明。可以多次声明和定义同一命名空间,每次给这一命名空间添加新成员。同名的命名空间即便在声明位置不同,仍然是同一个实体。可以在一个命名空间中引入其他命名空间的成员。

    C++11起支持内联命名空间。使用inline namespace作为声明的起始。内联命名空间的名称在名称查找时被特别对待,使用qualified-id引用其中的名称时,被内联的命名空间名称可以省略。也即,内联命名空间内的标识符被提升到包含着内联的命名空间的那个父级的命名空间中。内联命名空间可以在修改命名空间名称的同时避免在二进制文件中生成的符号改变,因此不同内联命名空间的名称可以用于标识接口兼容的不同版本,有助于保持二进制兼容性。这也在标准库的实现中被使用,如libstdc++和libc++。

    下面是从其他文章中copy的测试代码,详细内容介绍可以参考对应的reference:

    #include "namespace.hpp"
    #include <iostream>
    #include <vector>
    #include <string>
    
    namespace namespace_ {
    
    
    // reference: http://en.cppreference.com/w/cpp/language/namespace
    namespace A {
    	int x;
    }
    
    namespace B {
    	int i;
    	struct g { };
    	struct x { };
    	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
    	void f(double) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
    	void g(char) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); }; // OK: function name g hides struct g
    }
    
    int test_namespace_1()
    {
    	int i;
    	//using B::i; // error: i declared twice
    
    	void f(char);
    	using B::f; // OK: f(char), f(int), f(double) are overloads
    	f(3.5); // calls B::f(double)
    
    	using B::g;
    	g('a');      // calls B::g(char)
    	struct g g1; // declares g1 to have type struct B::g
    
    	using B::x;
    	using A::x;  // OK: hides struct B::x
    	x = 99;      // assigns to A::x
    	struct x x1; // declares x1 to have type struct B::x
    
    	return 0;
    }
    
    /
    // reference: http://en.cppreference.com/w/cpp/language/namespace
    namespace D {
    	int d1;
    	void f(char) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
    }
    using namespace D; // introduces D::d1, D::f, D::d2, D::f,
    //  E::e, and E::f into global namespace!
    
    int d1; // OK: no conflict with D::d1 when declaring
    namespace E {
    	int e;
    	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
    }
    namespace D { // namespace extension
    	int d2;
    	using namespace E; // transitive using-directive
    	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
    }
    
    int test_namespace_2()
    {
    	//d1++; // error: ambiguous ::d1 or D::d1?
    	::namespace_::d1++; // OK
    	D::d1++; // OK
    	d2++; // OK, d2 is D::d2
    	e++; // OK: e is E::e due to transitive using
    	//f(1); // error: ambiguous: D::f(int) or E::f(int)?
    	f('a'); // OK: the only f(char) is D::f(char)
    
    	return 0;
    }
    
    //
    // reference: http://en.cppreference.com/w/cpp/language/namespace
    namespace vec {
    
    	template< typename T >
    	class vector {
    		// ...
    	};
    
    } // of vec
    
    int test_namespace_3()
    {
    	std::vector<int> v1; // Standard vector.
    	vec::vector<int> v2; // User defined vector.
    
    	//v1 = v2; // Error: v1 and v2 are different object's type.
    
    	{
    		using namespace std;
    		vector<int> v3; // Same as std::vector
    		v1 = v3; // OK
    	}
    
    	{
    		using vec::vector;
    		vector<int> v4; // Same as vec::vector
    		v2 = v4; // OK
    	}
    
    	return 0;
    }
    
    ///
    // reference: https://msdn.microsoft.com/en-us/library/5cb46ksf.aspx
    /*namespace Test {
    	namespace old_ns {
    		std::string Func() { return std::string("Hello from old"); }
    	}
    
    	inline namespace new_ns {
    		std::string Func() { return std::string("Hello from new"); }
    	}
    }
    
    int test_namespace_4()
    {
    	using namespace Test;
    	using namespace std;
    
    	string s = Func();
    	std::cout << s << std::endl; // "Hello from new"
    	return 0;
    } */
    
    ///
    // reference: https://www.tutorialspoint.com/cplusplus/cpp_namespaces.htm
    // first name space
    namespace first_space {
    	void func() {
    		std::cout << "Inside first_space" << std::endl;
    	}
    
    	// second name space
    	namespace second_space {
    		void func() {
    			std::cout << "Inside second_space" << std::endl;
    		}
    	}
    }
    
    int test_namespace_5()
    {
    	using namespace first_space::second_space;
    
    	// This calls function from second name space.
    	func();
    
    	return 0;
    }
    
    /
    // reference: http://www.geeksforgeeks.org/namespace-in-c/
    // A C++ code to demonstrate that we can define methods outside namespace.
    // Creating a namespace
    namespace ns {
    	void display();
    
    	class geek {
    	public:
    		void display();
    	};
    }
    
    // Defining methods of namespace
    void ns::geek::display()
    {
    	std::cout << "ns::geek::display()\n";
    }
    
    void ns::display()
    {
    	std::cout << "ns::display()\n";
    }
    
    int test_namespace_6()
    {
    	ns::geek obj;
    	ns::display();
    	obj.display();
    
    	return 0;
    }
    
    } // using namespace_

    GitHub:  https://github.com/fengbingchun/Messy_Test 

    展开全文
  • c语言结构体学习整理(结构体初始化,结构体指针)

    万次阅读 多人点赞 2018-11-01 20:22:12
    7.1中我们已经提到结构体数组指针的命名,这里我们仅对一些知识做下介绍 这里我们接着来说结构体数组指针 在我们想要用指针访问结构体数组的第n个数据时可以用 struct Student stu1 [ 5 ] ; struct ...

    渣渣c的c语言学习之路

    1.关于c语言的结构体:

    首先我们为什么要用到结构体,我们都已经学了很多int char …等类型还学到了同类型元素构成的数组,以及取上述类型的指针,在一些小应用可以灵活使用,然而,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的,类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。

    2首先我们要了解一些小知识

    2.1**只有结构体变量才分配地址,而结构体的定义是不分配空间的。**
    2.2结构体中各成员的定义和之前的变量定义一样,但在定义时也不分配空间。
    2.3结构体变量的声明需要在主函数之上或者主函数中声明,如果在主函数之下则会报错
    2.4c语言中的结构体不能直接进行强制转换,只有结构体指针才能进行强制转换
    2.5相同类型的成员是可以定义在同一类型下的
    列如

    
    struct Student
    { 
    	int number,age;//int型学号和年龄
    	char name[20],sex;//char类型姓名和性别
    	float score;
    }

    最后的分号不要忘了 有的编译器会自动加上,因此有的同学就会不注意。

    3关于结构体变量的定义和引用

    在编译时,结构体的定义并不分配存储空间,对结构体变量才按其数据结构分配相应的存储空间

    
     struct Book
     { 
     	char title[20];//一个字符串表
    
    示的titile 题目
    	char author[20];//一个字符串表示的author作者
     	float value;//价格表示 
     };//这里只是声明 结构体的定义 
    struct Book book1,book2;//结构体变量的定义 分配空间
    
    book1.value;//引用结构体变量
    

    定义结构体变量以后,系统就会为其分配内存单元,比如book1和book2在内存中占44个字节(20+20+4)具体的长度你可以在你的编译器中使用sizeof关键字分别求出来。
    列如

    当然,要注意一点:用sizeof关键字求结构体长度时,返回的最大基本类型所占字节的整数倍 比方说我们上面求得的为44 为 float(4个字节)的整数倍,
    但是我们把title修改为title[22]; 这时正常长度为46 ,但是你会发现实际求得的为48,(4的整数倍)

    这就涉及到结构体的存储

    1结构体整体空间是占用空间最大的成员(的类型)所占字节数的整数倍。

    2.结构体的每个成员相对结构体首地址的偏移量(offset)都是最大基本类型成员字节大小的整数倍,如果不是编译器会自动补齐,

    关于这个我们简单介绍下:

    1.偏移量----偏移量指的是结构体变量中成员的地址和结构体变量首地址的差。即偏移字节数,结构体大小等于最后一个成员的偏移量加上他的大小,第一个成员的偏移量为0,

    struct S1
    {
        char a;
    
        int b;
    
        double c;
    };
    

    这里char a 偏移量为1 之后为int b 因为偏移量1不为int(4)的整数倍,所以会自动补齐,而在 double c 时,偏移量为8 是double(8)的整数倍,所以不用自动补齐 最后求得结构体得大小为 16

    具体看下图:
    在这里插入图片描述
    通过上面的代码同学们应该会有一个简单的认知

    4结构体变量的初始化

    结构体的初始化有很多需要注意的地方,这里我们说明下
    首先是几种初始化的方法
    ps在对结构体变量初始化时,要对结构体成员一一赋值,不能跳过前面成员变量,而直接给后面成员赋初值,但是可以只赋值前面几个,对与后面未赋值的变量,如果是数值型,则会自动赋值为0,对于字符型,会自动赋初值为NULL,即‘\0’

    4.1定义时直接赋值

    struct Student
    { 
    	char name[20];
    	char sex;
    	int number;
    }stu1={"zhaozixuan",'M',12345};
    //或者
    struct Student
    { 
    	char name[20];
    	char sex;
    	int number;
    }struct Student stu1={"zhaozixuan",'M',12345};
    
    

    注意字符为‘ ’ 字符串为""
    4.2定义结构体之后逐个赋值

    stu1.name="王伟";
    stu1.sex='M';
    stu1.number=12305;
    //也可用strcpy函数进行赋值
    strcpy(stu1.name,"王伟");
    
    

    4.3定义之后任意赋值

     struct Student stu1={
      .name="Wang",
      .number=12345,
      .sex='W', 
     };//可以对任意变量赋值
    

    这样写的好处时不用按照顺序来进行初始化,而且可以对你想要赋值的变量直接进行赋值,而不想赋值的变量可以不用赋值

    需要注意的是如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了;

    等下结构体数组初始化时我们还会有一个讲解

    这里我们顺带提一下typedef说明结构体类型


    这里的BOOK就相当于struct book的一个别名一样,用它来定义结构体变量非常简便
    主要也是考二级要用到,所以我们简单介绍下

    5结构体变量的引用(输出和输入)

    5.1结构体变量的赋值用scanf赋值和printf输出时跟其他变量操作一样
    但是有几点需要注意
    (1) .是运算符,在所有运算符优先级中最高
    (2)如果结构体的成员本身是一个结构体,则需要继续用.运算符,直到最低一级的成员。

    struct Student
    {	char name[20];
    	char sex;
    	int number;
    	struct Date
    	{
    		int year;
     		int month;
     		int day;
    	}birthday;
    
    }stu1;
    printf("%d",stu1.birthday);//这样子是错误的,因为birthday也是一个结构体变量
    scanf("%d",&stu1.birthday.month);//正确
    

    (3)可以引用接头体变量成员的地址,也可以引用结构体变量的地址:

    printf("%o", student);(输出student的首地址)(%o 按八进制输出)

    6结构体数组及其初始化(重点)

    这里我们简单说下,具有相同类型的结构体变量组成数组就是结构体数组

    结构体数组与结构体变量区别只是将结构体变量替换为数组

    struct Student
    { 
    	char name[20];
    	char sex;
    	int number;
    }stu1[5]={
    	 {"zhaozixuan",'M',12345},
    	 {"houxiaohong",'M',12306},
    	 {"qxiaoxin",'W',12546},
    	 {"wangwei",'M',14679},
    	 {"yulongjiao",'W',17857}
    };
    stu1[3].name[3]//表示stu1的第三个结构变量中姓名的第五个字符
    //若初始化时已经是结构体数组全部元素[]中的数可以不写如stu1[]=
    

    注意结构体数组要在定义时就直接初始化,如果先定义再赋初值是错误的
    比如:

    struct Student stu1;
    stu1[3]={
      {"zhaozixuan",'M',12345},
      {"houxiaohong",'M',12306},
      {"qxiaoxin",'W',12546}
      };
      
    

    这样子是错误的,

    这里我在写的时候遇到一些问题,还是结构体数组初始化的问题,折腾了下解决了,给大家分享下
    对于数组初始化时
    比如

    char str[20];
    str="I love you";/* 这样会修改数组的地址,但是数组的地址分配之后是不允许改变的 */
    
    

    在第一条语句中 str就已经被定义成数组而在C99标准中不允许将字符串(实际上是一个指针变量) 赋值给数组,所以如果我们直接赋值是错误的

    那么怎么弄呢
    这里提供3种方法

    1.定义数组时直接定义
    char str[20]=“I love you”;

    2.用strcpy或者memset函数进行复制
    char str[20];
    strcpy(str,“I love you”);
    再用到memset函数时,出现了一些问题
    对于memcset函数简单介绍下

    memset
    void *memset(void *s,int c,size_t n)
    作用:将已开辟内存空间s的首n个字节的值设为值c。

    char str[20];
    memset(str,'a',20);
    

    如果是字符类型数组的话,memset可以随便用,但是对于其他类型的数组,一般只用来清0或者填-1,如果是填充其他数据就会出错

    int str[10];
    memset(str,1,sizeof(str));//这样是错误的
    
    

    这里我们说下这个错误,

    首先我们要知道memset在进行赋值时,是按字节为单位来进行赋值的,每次填充的数据长度为一个字节,而对于其他类型的变量,比如int,占4个字节 所以sizeof(str)=40; 而用memset赋值时,将会对指向str地址的前40个字节进行赋值0x01(00000001) 的操作,把0x00000000赋值4次0x01操作变为0x01010101(00000001000000010000000100000001)

    相当于给“前10个int”进行了赋值0x01010101的操作 对应十进制的16843009
    所以会出很大的错误

    这里请务必要注意,但是如果是清零一个数组用memset还是很方便的
    简单使用的话同学们用strcmp函数就行

    3用指针(注意内存分配)
    char *str;
    str=“I love you”;

    这两句话的本质是,在内存中开辟一段内存空间,把"I love you"放进这段内存空间,然后把这段内存空间的地址交给str,由于str是变量,所以给它赋值是合法的。

    请注意,在我们进行数组初始化的时候如果定义的数组过长,而我们只初始化了一部分数据,对于未初始化的数据如果是数值型,则会自动赋值为0,对于字符型,会自动赋初值为NULL,即‘\0’ 即不足的元素补以默认值
    这里我们在4小节中也提到了
    比如

    int str[10]={1};//这里只是把str的第一个元素赋值为1,其他元素默认为0
    
    



    7结构体与指针

    我们知道,指针指向的是变量所占内存的首地址,在结构体中,指针指向的是结构体变量的起始地址,当然也可指向结构体变量的元素
    这里我们分为三部分
    7.1指向结构体变量的指针

    定义形式一般为
    struct 结构体名* 指针名;
    比如: struct Student* p;

    struct Student
    {	
    	char cName[20];
     	int number;
     	char csex;  
    }student1;
    struct Student*p;
    p=&student1;
    //若为结构体数组则
    struct Student stu1[5];
    struct Student*p;
    p=stu1;//因为stu1为结构体数组而p=stu1直接是指向stu1的首地址,就不用再加&符
    
    

    用结构体指针变量访问结构体变量成员有以下两种方式:
    (*p).cName //这里的括号不能少,在5.1中有提到
    p->cName

    简单来说以下三种形式是等价的

    p->cName
    (*p).cName 
    student1.cName
    p->cName //可以进行正常的运算
    

    p->number++; 是将结构体变量中number的值进行运算,然后再加一,
    这里要注意下,等下在7.2中会有比较

    7.2指向结构体数组的指针

    7.1中我们已经提到结构体数组指针的命名,这里我们仅对一些知识点做下介绍
    这里我们接着来说结构体数组指针
    在我们想要用指针访问结构体数组的第n个数据时可以用

    struct Student stu1[5];
    struct Student*p;
    p=stu[n];
    (++p).number//是指向了结构体数组下一个元素的地址
    
    

    7.3结构体成员是指针类型变量
    比如

    struct Student
    {
     	char* Name;//这样防止名字长短不一造成空间的浪费
     	int number;
     	char csex;  
    }student1;
    
    

    在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的,但是Name会被分配到任意的一的地址,结构体不为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险,

    struct Student
    {
     	char* Name;
     	int number;
     	char csex;  
    }stu,*stu;
    
    stu.name=(char*)malloc(sizeof(char));//内存初始化
    

    这里我们说一下,同学们看书的时候一般不会看到,
    如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存

    struct Student
    {
     	char* Name;
     	int number;
    	char csex;  
    }stu,*stu;
    stu = (struct student*)malloc(sizeof(struct student));./*结构体指针初始化*/
      stu->name = (char*)malloc(sizeof(char));/*结构体指针的成员指针同样需要初始化*/  
    
    

    7.4二叉树遍历算法
    二叉树的二叉链表类型定义如下:
    typedef struct btnode {
    datatype data;
    struct btnode *lchild,*rchild;
    };
    这里我们仅仅提出以下,因为涉及到链表,感兴趣的同学可以去学习下(二级要用),
    7.5结构体作为函数参数

    首先我们要注意的一点,使用结构体变量作为函数参数的时候,采取的是值传递的方式,将结构体所占内存单元的内容全部传递给形参,并且形参必须也要是同类型的结构体变量,在使用时,会自动创建一个结构体变量作为原变量的副本,并且也需要占内存,并且在调用期间如果修改(形参)结构体中成员的值,修改值是无效的

    而如果用指针作为实参,传递给函数的形参,这时候传递的是结构体的地址,形参所指向的地址就是结构体变量的地址,这时候进行修改的话是可以修改的,这正是指针的精华所在

    在这里我们再提供几种互换两个结构体的方法

    struct Student
    {
     char cName[20];
     int number;
     char csex;  
    }student1,student2;
    struct Student student1={"Wang",12345,'W'};
    struct Student student2={"Zhao",54321,'M'}; 
    struct Student*stu1=&student1;
    struct Student*stu2=&student2;
    
    struct Student *student3;
    student3=stu1;
    stu1=stu2;
    stu2=student3;//互换地址
    
    2对于同类型结构体直接互换值就行
    struct stu student3;
    student3=student1;
    student1=student2;
    student2=student3;
    //这里也可以写成应strcmp函数互换
    
    3memcpy()函数进行互换
    
    
    4比较笨的方法: 用for循环互换
    

    最后提下memset清空结构体

    struct Student
    {
     char cName[20];
     int number;
     char csex;  
    }stu1;
    
    一般情况下,清空str的方法:
      str.cName[0]='\0';
      str.csex='0';
      str.number=0;
      但是我们用memset就非常方便:
      memset(&str,0,sizeof(struct Student));
      如果是数组:
      struct Student stu[10];
      就是
      memset(stu,0,sizeof(struct Student)*10);
    

    整理不易,点个赞再走呗!

    展开全文
  • C语言

    万次阅读 多人点赞 2019-12-18 23:01:50
    椭圆形为连接。 5.一个c语言只能有一个主函数。 6.函数的定义不可以嵌套,函数的调用可以嵌套。 7.C语言总是以main函数开始执行。 8.常量的类型:整型常量、实型常量、字符常量、字符串常量、符号常量。 9.十进制...

    公共考点
    1.算法的空间复杂度是指算法在执行过程中所需要的内存空间。
    2.算法的时间复杂度是指算法所需要的计算工作量。
    3.数据的逻辑结构与储存结构不是一一对应的。
    4.队列的修改是以先进先出的原则进行的。–与队列结构有关联的是先到先服务的作业调度。
    5.循环队列中的元素个数随队头指针和队尾指针变化而动态变化。
    6.C语言中的result只是一个自己定义的量
    7.对空和队满时,头尾指针均相等。
    8.冒泡法是在扫描过程中逐次比较相邻两个元素的大小。例:9+8+7+6+5+4+3+2+1=45.
    9.对象间的信息传递靠消息。
    10.多态性是指同一个操作可以是不同对象的行为。操作—对象。

    C语言
    1.源程序的扩展名为.c,目标程序的扩展名为.obj,可执行程序的扩展名为.exe(每个后缀为.c的C语言都可以单独进行编译)(C语言编译程序把.c编译成.obj的二进制文件)(链接形成.exe文件)
    2.循环结构、选择结构,顺序结构都是结构化程序的基本结构。
    3.N-S流程图是复杂算法的描述手段。
    4.长方形为处理框。椭圆形为连接点。
    5.一个c语言只能有一个主函数。
    6.函数的定义不可以嵌套,函数的调用可以嵌套。
    7.C语言总是以main函数开始执行。
    8.常量的类型:整型常量、实型常量、字符常量、字符串常量、符号常量。
    9.十进制整型常量:基本数字范围:0-9;(十进制小数两边必须有数字)
    八进制整型常量:以0开头,输出格式控制符为%o,基本数字范围0-7;
    十六进制整型常量:以0x开头,输出格式为%x,基本数字范围为0-15写为A-F或a-f;
    指数形式:e前必须有数字,e后必须为整数。
    10. 关键字属于标识符。(关键字不能做变量名也不能做函数名)
    11.数值型常量有整型常量、实型常量但均有正负值之分。
    12.语言的预编译处理可以可以用符号名代表一个常量定义是不必指定常量类型。
    13.实型常量又称实数或浮点数。在C语言中可以用单精度型和双精度型两种形式表示实型常量,分别用类型名float和double进行定义。实型常量在一般的微型集中占用4个字节,一般形式或者指数形式,数值范围都是-1038~1038,有效数字是7位。(不能是整形数据,如0)(常量的类型可以从字面上区分)(1为整型常量)(1.0为实型常量)(a为字符型常量)
    14.\0为八进制数,所以\09是错误的。
    15.字符常量在内存中占1个字节,字符常量可以进行关系运算。不能参与数值运算,可以参与任何整数运算。
    16.不能用字符串常量对字符数组名进行整体赋值操作。
    17.可以使用字符串常量来给一维数组进行复制。
    18.关于字节大小的问题

    16位编译器:char 1个字节  char* 2个字节 int 2个字节 float 4个字节 double 8个字节
    32位编译器:char 1个字节  char* 2个字节 int 4个字节 float 4个字节 double 8个字节
    64位编译器:char 1个字节  char* 2个字节 int 4个字节 float 4个字节 double 8个字节
    

    19.10进制转8进制,手算用 除8取余数法得
    20.十进制转十六进制为:除十六取余直到商为0,余数从后往前读。
    21.%f代表单精度浮点型数据(float),%lf代表双精度浮点型数(double)。
    单精度浮点数有效数字保证6位,部分7位,双精度浮点数有效数字保证15位,部分16位。
    22.sizeof可以看成是一个无符号整型表达式(sizeof为字节运算符)
    23.强制运算符:(类型名)(表达式) 逗号运算符:, 条件运算符::? :
    24. 赋值运算符左边必须是(一个)变量。
    25.a=bc,先运算bc,这个表达式的含义是,若b与c相等,那么得出的值为1,若不等则为0.
    26.“^” 按位异或 两数的二进制对应位相同,则为0,不同则为1.
    27.“|” 按位或 两个二进制中只要有一个为1,则结果为1。
    28.“~” 按位取反 二进制 0变1,1变0.
    29. “&”按位与 两个二进制都为1,则该位的结果为1,否则为零
    【 零的按位取反是 -1(0在数学界既不是正数也不是负数)
    所有正整数的按位取反是其本身+1的负数
    所有负整数的按位取反是其本身+1的绝对值 】
    30.位运算的对象只能是整形或字符型数据
    31.||逻辑或 前后条件只要有一个满足则为真。
    32.&&逻辑与 前后条件同时满足表达式为真。
    33.再用||的地方一般可以用|代替,但是用|的地方不能用||代替。
    34.“&”取地址运算
    35“”指针运算符
    36.p是指针变量,则&p是变量p的地址
    37.p是指针变量,则
    p是变量p所指向地址的值
    38.基类型不同的指针变量不可以相互混用
    39.函数的类型可以是指针类型
    40.函数的参数可以是整型、实型、字符型、指针类型。
    41.在这里插入图片描述

    42.C语言是一种计算机高级语言。
    43.C语言允许直接访问物理地址,能进行位操作。
    44.C语言是结构化程序设计语言
    45.c程序要通过编译,连接才能得到可执行的目标程序
    46.用c语言编写程序,可以编写出任何类型的程序
    47.C语言允许有空函数
    48.C程序书写格式,允许一行内可以写几个语句
    49.C程序的语句无行号(C语言中给源程序加行号;行号是用来定位代码的,指文件在几行)
    50.C语言的每个语句的最后必须有一个分号
    51.C语言本身没有输入输出语句(没有特定的输入输出语句)
    52.C语言可用来编写应用软件,也可用来编写系软件
    53.TurboC是在微机上广泛使用的编译程序
    54.C语言的数据结构是以数据类型形式出现的(不是常量和变量)
    55.空类型是C语言的一种数据类型
    56.C语言中数据有常量和变量之分
    57.利用指针和结构体类型可以构成表、树等复杂的数据结构
    58.在C程序中对所用到的所有数据都必须指定其数据类型
    59.c程序运行过程中,其值不能被改变的量称为常量
    60.在程序运行过程中,其值可以改变的量称为变量
    61.C语言可以用一个标识符代表一个常量,称为符号常量
    62.C语言规定标识符只能由字母、数字和下划线三种字符组成
    63.C语言整型常量可用十进制整数、八进整数和十六进制整数三种形式表示
    64.在现微机上使用的C编译系统,每一个整型变量在内存中占2个字节
    65.整型变量的基本类型符为int
    66.在微机上,一个长整型变量在内存中占4个字节(float型变量在内存中占4个字节)
    67.一个int型变量的最大允许值为32767
    68.在一个整常量后面加一个字母“L”或“1”.则认为该常量是longint 型常量
    69.C语言实型常量可用二进制小数和指数二种形式表示
    70.C语言实型变量分为:float型、double型、long double型三类
    71.C语言doule型一个变量的数值有效数字是16位
    72.C语言的字符常量是用单引号括起来的一个字符
    73.C语言的转义字符是以一个“\”开头的一种特殊形式的字符常量
    74.C语言中换行符使用’\n’,这是一个转义字符
    75.转文字符\r的含义是回车。
    76.C语言的字符型变量只能存放一个字符
    77.C语言允许字符数据与整数直接进行算术运算
    78.C语言允许在定义变量的同时使变量初始化
    79.C语言允许整型、实型、字符型数据间可以混合运算
    80.C语言规定两个整数相除的结果为整数
    81.用求余运算符“%”作运算,运算符两侧均应为整型数据
    82.用算术运算符和括号将运算对象按C语法规则组成的式子,称为C算术表达式
    83.算术运算符的结合方向为“自左至右”
    84.强制类型转换时,原来变量的类型未发生变化
    85.自增、自减运算符的结合方向为“自右至左”
    86.自增运算符只能用于变量,不能用于常量或表达式
    87指针.自增(减)运算符也可以用于指针变量,使指向下一个地址
    88.运算符“=”的作用是将一个数据赋给一个变量
    89.运算符“”的作用是将两侧数据是否相等
    90.赋运算符的结合方向是“自右向左”
    91.凡是二目运算符,都可以与赋值运算符一起组合成复合赋值运算符
    92.运算符“
    ”的作用是将一个数据赋给一个变量
    93.C语言不允许将实型数据赋给整型变量
    94.一个逗号表达式又可以与另一个表达式组成一个新的逗号表达式
    95.一个C程序可以由若干个源程序文件组成
    96.一个源文件可以由若千个函数和预处理命令以及全局变量声明部分组成
    97.空语句是C语言的一种语句
    98.复合语句中最后一个语句中最后的分号不能省略不写
    99.putchar函数的作用是向终端输出一个字符
    100.getchar函数的作用是从终端输入一个字符
    101.格式输出函数(print)一次可以输出多个数据
    102.printf函数的%ld格式参数,用来输入出长整型数据
    103.printf函数的%o格式参数,用来以8进制数形式输出整数
    104.printf函数的%f格式参数,用来以小数形式输出实数
    105.printf函数的%x格式参数,可以输出指定参数的16进制形式
    106.printf函数的%s格式参数,用来输出一个字符串
    107.C语言不是面向对象的程序设计语言
    108.printf函数的%e格式参数,以指数形式输出实数
    109.C语言单精度数的有效数一般为7位
    110.printf函数的%g格式参数
    111.%g是C语言printf()函数的一个输出格式类型,它表示以%f%e中较短的输出宽度输出单、双精度实数,在指数小于-4或者大于等于精度时使用%e格式
    112.p++是指下一个地址。
    (p)++是指将p所指的数据的值加一。
    C编译器认为
    和++是同优先级操作符,且都是从右至左结合的,所以p++中的++只作用在p上,和(p++)意思一样;在(p)++中,由于()的优先级比和++都高,所以++作用在()内的表达式*p上。比如有:
    int x,y,a[]={1,2,3,4,5},*p=a,*q=a;
    x=*p++;//执行这一句后x=a[0]=1,p=a+1
    y=(*q)++;//执行这一句后,y=a[0]+1=2,q仍然=a
    113. printf函数的附加格式说明字符“m”的含义是指输出数据的最小宽度
    114.scanf函数中的“格式控制”后面应当是变量地址(不是变量符)
    115.逻辑运算符>算术运算符>关系运算符>条件运算符>赋值运算符(罗算管调幅)
    116.条件运算符的结合方向是“自右向左"
    117.if语中又包含文可以转在电百度网点电 平句的嵌套
    118.条件运算符要求有3个操作对象,称为三目运算符
    119.条件表达式中三个表达式的类型可以不同
    120.switch语句是多分支选择语句
    121.switch语句中每一个case的常量表达式的值必须互不相同
    122.switch语句执行完一个case后面的语句后,流程控制转移到下一个case继续执行
    123.switch语句中多个case可以共用组执行语句
    124.goto语句为无条件转向语句
    125.C语句的循环语句中循环体如果包含一个以上的语句,必须以复合语句形式出现bre
    126.for循环语句中的3个表达式都可以省略
    127.C语句的一个循环体内允许又包含另一个完整的循环结构
    128.break语句不能用于循环语句和switch语句之外的任何其它语句中
    129.continue语句的作用是结束本次循环(而不是终止整个循环)
    130.C数组中的每一个元素都必须属于同一个数据类型
    131.C数组必须先定义,然后使用
    132.C语言规定只能逐个引用数组元素而不能一次引用整个数组
    133.在定义一维数组时可以只给一部分元素赋初值
    134.对二维数组初始化,可以分行给数组赋初值
    135.可以对二维数组的部分元素赋初值
    136.字符数组中的一个元素只存放一个字符
    137.如果一个字符数组中包含一个以上结束符’\0”,则遇第一个’\0’时输出就结束
    138.puts函数的作用是将一个字符串输出终端
    139.gets丽数的作用是从终端输入一个字符串到字符数组
    140.strlen 函数是测试字符串长度的函数
    141
    strcat函数是“字符串复制函数”。X
    strcpy函数是“字符串连接函数”。X
    strcmp函数是“字符串复制函数”。X
    strlwr函数是测试字符串长度的函数。X
    strupr函数是测试字符串长度的函数。X
    142.C程序一个函数可以被一个或多个函数调用多次
    143.一个C程序可由一个主函数和若干个其它函数构成
    144.C程序以源程序为单位进行编译(而不是函数)
    145.C程序由一个或多个源程序文件组成
    146.C语言在定义函数时是互相独立的,不能嵌套定义
    147.在调用有参函数时,主调函数和被调用函数之间有数据传递关系
    148.在调用一个函数的过程中又出现直接或间接地调用该函数本身称为函数的递归调用
    149.在一个函数内部定义的变量是内部变量,称为局部变量
    150.在函数之外定义的变量称为外部变量,是全局变量
    151.从变量的作用域角度来分,可以分为全局变量和局部变量(而不是静态和动态变量)
    152.静态存储方式是指在程序运行期间分配固定的存储空间的方式
    153.存储方法分为两大类:静态存储类和动态存储类
    154.C语言允许将局部变量的值放在CPU中的寄存器中,这种变量称为“寄存器变量”
    155.局部静态变量不能定义为寄存器变量
    156.如果一个函数只能被本文件中其它函数所调用,称为内部函数
    157.C源程序中的预处理命令,它不是C语言本身的组成部分
    158.宏定义不是C语句,在行末不加分号
    159.宏定又是用宏名代替一个字符串,只作简单的置换,不作正确性检查
    160.在进行宏定义时,可以引用已定义的宏名
    161.宏替换不占程序运行时间,只占编译时间
    162.文件包含处理是指个源文件可以将另一个的全部内容含进来源文件包
    163.一个include命令只能指定一个被包含文件
    164.存放变量地址的变量是指针变量
    165.C语言中变量的指针就是变量的地址
    166.函数的参数也可以是指针变量
    167.指针变量可以指向变量,也可以指向数组和数组元素
    168.引用数组元素可以用下标法,也可以用指针法
    169.用指针变量可以指向一维数组,也可以指向多维数组,用指针变量也可以指向一个函数
    170.一个函数可以带回一个整型值、字符值或实型值,也可以带回指针型的数据
    171.指针数组中的每一个元素都相当于一个指针变量
    172.指针数组中的每一个元素都相当于一个整型变量
    173.指针变量可以有空值,即该指针变量不指向任何变量
    174.若两个指针指向同一个数组的元素。则两指针变量可以进行比较
    175.用户自己定义一个结构教型后们其中并无具体数据
    176.在程序中使用的可和百网时 定义结构体类型的变量
    177.结构体类型的成员也可以是一个结构体变量
    178.结构体成员名可以与程序中的变量名相同
    179.不能将一个结构体变量作为一个整体进行输入和输出
    180.对结构体变量的成员可以像普通变量一样进行各种运算
    181.可以引用结构体变量的地址
    182.可以引用结构体变量成员的地址
    183.结构体数组的每个元素都是一个个结构体类型的数据
    184.对结构体数组可以初始化,即赋初值
    185.可以定义一个指针变量,用来指向一个结构体变量
    186.指针可以用结构体变量作链表中的结点
    187.malloc函数的返回值是一个指向分配域起始地址的指针
    188.建立动态链表是指在程序执行过程中从无到有地建立起一个链表
    189.使几个不同的变量共占同一段内存的结构,称为共用体类型的结构
    190.共用体变量所占的内存长度等于最长的成员长度
    191.定义了共用体变量,只能引用共用体变量中的成员(不能引用公用体变量)
    192.共用体变量的地址和它的各成员的地址都是同一地址
    193.共用体类型可以出现在结构体类型定义中
    194.结构体类型可以出在共用体类型定义中
    195.在C编译中,对枚举元素按常量处理
    196.一个整数不能直接赋给一个枚举变量
    枚举类型在C#或C++,java,VB等一些计算机编程语言中是一种基本数据类型而不是构造数据类型,而在C语言等计算机编程语言中是一种构造数据类型 。它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
    枚举可以根据Integer、Long、Short或Byte中的任意一种数据类型来创建一种新型变量。这种变量能设置为已经定义的一组之中的一个,有效地防止用户提供无效值。该变量可使代码更加清晰,因为它可以描述特定的值。
    197.可以用typedef声明新的类型名来代替已有的类型名
    198.位运算的运算量只能是整型或字符型的数据
    200.位运算符与赋值运算符可以组成复合赋值运算符
    在 C 语言中, 一种方法是用叫做位段的构造类型来定义一个压缩信息的结构。
    201.已有定义int (*p)( );指针p可以指向函数的入口地址
    202.C语言中运算对象必须是整型的是%=
    203.int *p 表达的是p是指向int型数据的指针。
    204函数rewind的功能是将文件指针重新指向一个流的开头(即使文件指针重新返回文件的开始位置),int rewind(FILE *stream);并且无返值。
    205.如果函数值的类型与返回值类型不一致,以函数值类型为准
    206.c语言中形参和实参类型不一致时以形参的类型为准
    207.形参应该是函数声明的时候就已经定义好
    208.若有定义int t[3][2],能正确表达t数组元素地址的是–t[2]+1
    209.int[]={1,2};
    210.C语言中的循环语句有for,while,do-while和goto,,***不是if、switch、break
    211.不正确的赋值语句是—ch‘a+b’,正确的是ch=‘\0’ ch=‘7’+‘9’ ch=7+9
    212.正确的赋值语句x3=12;
    213.C语言逻辑运算时,0为假,非0为真
    214.字符串常量是以双引号扩起来的字符序列“a”(其他C语言常量‘\n’ 012)(e-2不是C语言常量----实数的指数形式中,e后面必须有一个整数)

    301.一个位段必须存储在同一存储单元中
    302.位段的长度不能大于存储单元的长度
    303.一个c程序由若干个函数构成,其中有且仅有一个主函数
    304.指针变量中存放的是它所指对象的地址
    305.在C语言中,分号是语句的必然组成部分
    306.结构体变量所占空间是各成员所占空间之和
    307.数据文件可顺序读取,也可借助文件的定位操作实现随机读取
    308.从用户的角度上讲,类型的含义是规定了该类型变量的取值范围和运算范围
    309.c语言中,变量和函数均具有类型和存贮类别两个属性
    340.顺序结构>选择结构>循环结构
    341.函数返回值的类型是由函数定义时指定的类型
    342.*与s[]相等
    343.当从键盘输入数据时,对于整型变量可以输入整型数值和字符,对于实型变量可以输入实型数和整型数值等。
    344. getchar函数没有参数
    345.静态储存方式是在程序运行期间分配固定的储存方式的方式
    356.局部静态变量不能定义为寄存器变量
    357.不能把共用体变量作为函数的参数
    358.一个整数不能直接赋给一个枚举变量
    359.int *p=a 是对指针变量p的正确定义和初始化。
    360.Char s[]=”china”;
    Char p;
    P=s;
    p与s[]相等
    有int [],*p=a
    则p+5表示元素a[]的地址
    361.C语言中,退格符是\b
    362.C语言中,变量的隐含储存类别是auto
    363.实际参数和形式参数可以同名
    364.函数调用可以作为一个函数的形参
    365.结构化程序设计的3中结构是-顺序结构、选择结构、循环结构
    366.当从键盘输入数据时整型变量可以输出整型值和字符,对于实型变量可以输入实型数和整型数值
    367.C语言中逗号运算符的优先级最低,指针最优,单目运算优于双目运算。如正负号。
    先算术运算,后移位运算,最后位运算。请特别注意:1 << 3 + 2 & 7等价于 (1 << (3 + 2))&7.
    逻辑运算最后结合。
    368.C语言区分定义变量名的大小写
    369.设有如下定义:
    struck sk
    { int a;
    float b;
    } data;
    int *p;
    若要使P指向data中的a域,正确的赋值语句是(C)A、 p=&a; B、 p=data.a; C、 p=&data.a; D、 *p=data.a;
    370.double)a是将a转换成double类型;(int)(x+y)是将x+y的值转换成整型。
    371.设有以下说明语句:
    struct stu
    {
    int a;
    float b;
    }
    stutype;
    则下面叙述不正确的是( )。
    A) struct是结构体类型的关键字
    B) structstu是用户定义的结构体类型
    C) stutype是用户定义的结构体类型名
    D) a和b都是结构体成员名
    答案解析
    定义一个结构的一般形式为:
    struct结构体名
    {
    成员列表
    }变量名列表;
    本题中的stutype是在声明结构体类型structstu的同时定义的该结构体变量,而不是用户定义的结构体类型名。类型与变量是不同的概念; 2)对结构体中的成员,可以单独使用,它的作用与地位相当于普通变量;3)成员也可以是一个结构体变量; 4)成员名可以与程序中的变量名相同,二者不代表同一对象。
    372.C语言中的数据类型是指-函数返回值的数据类型
    373.C程序设计语言的基本成分是数据成分、运算成分、控制成分、传输成分。
    374.while(t=1)循环控制表达式的值为1。
    375.printf(++x);表示地址所连接的数值加1.
    376.int[3][4]; 表示a为3行
    4列的数组,它可用的最大行下标为2,列下标最大为3;
    若是引用a[0][4],则超过了数组的范围
    377.若有如下说明和定义
    struct test
    {
    int ml; char m2; float m3;
    union uu
    {
    char ul[5]; int u2[2];
    }
    ua;
    } myaa;
    则sizeof(struct test)
    的值是A.12 B.16 C.14 D.9
    正确答案:A
    在本题中,首先定义了一个结构体。在该结构体中,定义了一个整型变量成员、一个字符型变量成员和一个浮点型变量成员,并在结构体中定义了一个联合体变量成员,联合体变量成员中又包含两个联合体成员数组。题目最后要求计算该结构体变量所占的存储空间。
    在C语言中,联合体变量中的所有成员共享存储空间,联合变量的长度等于各成员中最长的长度,因此,本题的联合体部分所占的长度为5,但是结构体与联合体不一样的是,结构体不能共享空间,一个结构体变量的总长度是各成员长度之和,因此,该结构体所需的存储空间为5+1+2+4=12。本题的正确答案选A。
    378.静态储存类别的关键词是static
    379.C语言中提供了存储说明符auto,register,extern,static说明的四种存储类别。四种存储类别说明符有两种存储期:自动存储期和静态存储期。其中auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块是被建立,它在该程序块活动时存在,退出该程序块时撤销。
    380.fseek(文件指针,位移量,起始点)
    “起始点”用0,1或2代替,0代表“文件开始”,1为“当前位置”,2为“文件末尾”。“位移量”指以“起始点”为基点,向前移动的字节数。ANSIC和大多数C版本要求位移量是long型数据。这样当文件的长度大于 64k时不致出现问题。ANSI C标准规定在数字的末尾加一个字母L,就表示long型。
    381.若有定义:int (*p)[4];则标识符p ,是一个指针指向一个含有四个整形元素的一维数组。
    382.基本数据类型:整型、实型、字符型
    383.EOF是指向文本文件的结束标志,NULL是打开文件错误时的返回值。feof(fp)用来判断文件是否在文件末尾,文本文件和二进制文件均可以使用此函数,如果遇到文件结束就返回1,否则返回0。
    384.C语言的函数可以嵌套调用
    385.标准库函数fgets(s,n,f)的功能是什么–从文件f中读取长度不超过n-1的字符串存入指针s所指的内存。
    从流中读一行或指定个字符,
    原型是char *fgets(char *s, int n, FILE *stream);
    从流中读取n-1个字符,除非读完一行,参数s是来接收字符串,如果成功则返回s的指针,否则返回NULL。
    形参注释:*string结果数据的首地址;n-1:一次读入数据块的长度,其默认值为1k,即1024;stream文件指针
    说得简单一点就是从f这个文件输入流中读取n-1个字符,存到s中。
    如果一行的字符数小于n-1,那么就是一行的字符数,所以应该理解为不超过n-1,如果一行的长度大于n-1,就是n-1个字符
    386.
    1、数据计算类型不同。基本数据类型分为三类:整数型(定点型)、实数型(浮点型)和字符型。除了基本数据类型,还有构造类型(数组、结构体、共用体、枚举类型)、指针类型、空类型void。
    2、各种数据类型的关键词不同。short、long、int、float、double、char六个关键词表示C语言里六种基本数据类型。
    3、不同数据类型占用内存的大小不同。short占2byte,int占4byte,long占4byte,float占2byte,double占8byte,char占1byte(不同的平台可能占用内存大小不一样,具体的可以用sizeof 测试下)。
    387.一个可以没有变量定义和执行部分,例如空函数

    展开全文
  • namespace命名空间-之使用

    千次阅读 2017-05-31 21:12:09
    由于各种原因,用户空间命名空间的实现算是一个里程碑了,首先是历时五年最复杂命名空间之一的user命名空间开发完结,其次是内核绝大多数命名空间已经实现了,当前的命名空间可以说是一个稳定版本了,但这并不意味着...

    文章下载地址    http://download.csdn.net/detail/shichaog/8185583

    由于各种原因,用户空间命名空间的实现算是一个里程碑了,首先是历时五年最复杂命名空间之一的user命名空间开发完结,其次是内核绝大多数命名空间已经实现了,当前的命名空间可以说是一个稳定版本了,但这并不意味着命名空间开发工作的完结,新的命名空间可能被添加进内核,也可能需要对当前的命名空间做一些扩展,例如内核日志的隔离。最后,就当前user命名空间的不同使用方法而言,可以说是“游戏规则”的改变:从3.8版本开始,非特权进程可以创建namespace,并且其拥有特权权限,下面将会展示编程中如何使用命名空间的API。

    命名空间

    目前Linux实现了六种类型的namespace,每一个namespace是包装了一些全局系统资源的抽象集合,这一抽象集合使得在进程的命名空间中可以看到全局系统资源。命名空间的一个总体目标是支持轻量级虚拟化工具container的实现,container机制本身对外提供一组进程,这组进程自己会认为它们就是系统唯一存在的进程。

    在下面的讨论中,按命名空间实现的版本先后依次对其介绍,当提到命名空间的API(clone(),ushare(),setns())时括号内的CLONE_NEW*用于标识命名空间的类型。

    mount命名空间 (CLONE_NEWS, Linux2.4.19)用于隔离一组进程看到的文件系统挂载点集合,即处于不同mount 命名空间的进程看到的文件系统层次很可能是不一样的。mount()和umount()系统调用的影响不再是全局的而只影响其调用进程指向的命名空间。

    mount命名空间的一个应用类似chroot,然而和chroot()系统调用相比,mount 命名空间在安全性和扩展性上更好。其它一些更复杂的应用如:不同的mount命名空间可以建立主从关系,这样可以让一个命名空间的事件自动传递到另一个命名空间。

    mount命名空间是Linux内核最早实现的命名空间,于2002年就开始了;这就是CLONE_NEWS的由来,当时没人想到其它不同的命名空间会被添加到内核。

    UTS命名空间(CLONE_NEWUTS, Linux2.6.19)隔离了两个系统变量,系统节点名和域名;uname()系统调用返回UTS,名字使用setnodename()和setdomainname()系统调用设置。从容器的上下文看,UTS赋予了每个容器各自的主机名和 网络信息服务名(NIS) (Network Information Service),这使得初始化和配置脚本能够根据不同的名字进行裁剪。UTS源于传递给uname()系统调用的参数:struct utsname。该结构体的名字源于"UNIX Time-sharing System"

    IPC namespaces (CLONE_NEWIPC, Linux 2.6.19)隔离进程间通信资源,具体来说就是System V IPC objects and (since Linux2.6.30) POSIX message queues;这些机制的共同特点是由其特点而非文件系统路径名标识。每一个IPC命名空间尤其自己的System V IPC标识符和POSIX 消息队列文件系统。

    PID namespaces (CLONE_NEWPID, Linux 2.6.24)隔离进程ID号命名空间,话句话说就是位于不同进程ID命名空间的进程可以有相同的进程ID号,PID命名空间的最大的好处是在主机之间移植container时,可以保留container内的ID号,PID命名空间允许每个container拥有自己的init进程(ID=1),init进程是所有进程的祖先,负责系统启动时的初始化和作为孤儿进程的父进程。

    从特殊的角度来看PID命名空间,就是一个进程有两个ID,一个ID号属于PID命名空间,一个ID号属于PID命名空间之外的主机系统,此外,PID命名空间能够被嵌套。

    Network namespaces (CLONE_NEWNET, Linux2.6.24开始结束于 Linux 2.6.29)用于隔离和网络有关的资源,这就使得每个网络命名空间有其自己的网络设备、IP地址、IP路由表、/proc/net目录、端口号等等。

    从网络命名空间的角度看,每个container拥有其自己的网络设备(虚拟的)和用于绑定自己网络端口号的应用程序。主机上合适的路由规则可以将网络数据包和特定container相关的网络设备关联。例如,可以有多个web 服务器,分别存在不同的container中,这就使得这些web 服务器可以在其命名空间中绑定80端口号。

    User namespaces (CLONE_NEWUSER, 起始于 Linux2.6.23 完成于 Linux 3.8) 隔离用户和组ID空间,换句话说,一个进程的用户和组ID在用户命名空间之外可以不同于命名空间之内的ID,最有趣的是一个用户ID在命名空间之外非特权,而在命名空间内却可以是具有特权的。这就意味着在命名空间内拥有全部的特权权限,在命名空间之外则不是这样。

    自Linux3.8开始,非特权进程可以创建用户命名空间,由于非特权进程在user命名空间内具有root权限,命名空间内非特权应用程序可以使用以前只有root能够使用的一些功能。

    总结

    自从第一个命名空间的实现到现在已有十年之久,命名空间的概念也发展为更通用的框架-隔离先前系统级的全局资源。结果使能命名空间能够提供完整的轻量级虚拟化系统,呈现的形式就是container。随着命名空间概念的扩展,与之相关的clone()系统调用和一两个/proc下的文件发展成许多其它的系统调用和/proc下更多的文件。这些扩展后的API成为本文接下来讨论的主题。

    系列博文索引

    下面的列表给出了后续的系列博文。

    Part 2: the namespaces API

    demo_uts_s.c:示例了UTS命名空间的使用方法。
    ns_exec.c: 使用setns()关联一个命名空间并且执行该命令。

    unshare.c: 停止命名空间的共享并执行命令。

     

    Part 3: PID namespaces

    pidns_init_sleep.c:证明PID命名空间
    multi_pidns.c:嵌套的PID命名空间中创建一系列的子进程。

     

    Part 4: more on PID namespaces

    ns_child_exec.c: 创建一个子进程在新的命名空间中执行命令
    simple_init.c: 简单的init类型的程序,用于在PID命名空间中使用。

    orphan.c: 证明一个子进程变为孤儿进程后,将被init进程收留

    ns_run.c: 使用setns()关联一个或多个命名空间,并且在这些命名空间中执行一个命令,使用场景很可能是在子进程中。

     

    Part 5: user namespaces

    demo_userns.c: 创建命名空间的简单程序,并且显示了程序的权限和能力
    userns_child_exec.c: 创建一个在一个新的命名空间执行shell命令的子进程,类似于ns_child_exec.c,但是提供了用户命名空间的额外选项。

     

    Part 6: more on user namespaces

    userns_setns_test.c:从连个不同的user命名空间测试setns()参数。

    Part 7: network namespaces

    命名空间 API

    一个命名空间包含一些抽象化的全局系统资源,这些隔离的全局资源在命名空间将呈现给进程。命名空间被用于很多目的,最突出的就是轻量级虚拟化容器(container)了。

    命名空间API包括三个系统调用:clone()、unshare()和setns(),此外,还包括/proc目录下的许多文件。本文将讨论上述系统调用以及/proc目录下的一些文件。为了明确使用的命名空间的类型,三个系统调用使用先前提到的CLONE_NEW*常量: 

    CLONE_NEWIPC, CLONE_NEWNS, CLONE_NEWNET, CLONE_NEWPID, CLONE_NEWUSER,and CLONE_NEWUTS。

    创建一个新的命名空间: clone()

    创建一个命名空间的方法是使用clone()系统调用,其会创建一个新的进程。为了说明创建的过程,给出clone()的原型如下:

    int clone(int(*child_func)(void *), void *child_stack, int flags, void*arg);

    本质上,clone()是一个通用的fork()版本,fork()的功能由flags参数控制。总的来说,约有超过20个不同的CLONE_*标志控制clone()提供不同的功能,包括父子进程是否共享如虚拟内存、打开的文件描述符、子进程等一些资源。如调用clone时设置了一个CLONE_NEW*标志,一个与之对应的新的命名空间将被创建,新的进程属于该命名空间。可以使用多个CLONE_NEW*标志的组合。

    我们的例子(demo_uts_namespace.c)调用clone()时设置了CLONE_NEWUTS标志以创建一个UTS命名空间。完整的demo_uts_namespaces.c

     


    #define _GNU_SOURCE
    #include <sys/wait.h>
    #include <sys/utsname.h>
    #include <sched.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    static int  childFunc(void *arg){
    
       struct utsname uts;
    
       if (sethostname(arg, strlen(arg)) == -1)
    
           exit(0);
    
       if (uname(&uts) == -1)
    
           exit(0);
    
       printf("uts.nodename in child: %s\n", uts.nodename);
     
       sleep(100);
    
       return 0;           /* Terminateschild */
    
    }
    
    
    #define STACK_SIZE (1024 * 1024)    /* Stack size for cloned child */
     
    static char child_stack[STACK_SIZE];
    
    int main(int argc, char *argv[]){
    
       pid_t child_pid;
       struct utsname uts;
       if (argc < 2) {
    
           fprintf(stderr, "Usage: %s <child-hostname>\n",argv[0]);
       }
     
       child_pid = clone(childFunc, child_stack +STACK_SIZE,CLONE_NEWUTS | SIGCHLD,argv[1]);
    
       if (child_pid == -1){
          
          exit(0);
       }
    
       printf("PID of child created by clone() is %ld\n", (long)child_pid);
    
       sleep(1);           
    
       if (uname(&uts) == -1){
        
           exit(0);
    
       }
    
       printf("uts.nodename in parent: %s\n", uts.nodename);
    
       if (waitpid(child_pid, NULL, 0) == -1){
       
           exit(0);
       }
          
       printf("child has terminated\n");
    
       exit(0);
    }
    


    例程需要一个命令行参数,当运行时,其创建一个在新的UTS命名空间执行的子进程。在新的命名空间中,子进程使用获得的命令行参数更改主机名。

    main函数第一个比较重要的部分是创建子进程的clone()调用。

       child_pid = clone(childFunc,

                        child_stack +STACK_SIZE,   /* Points to start of

                                                       downwardly growing stack */

                        CLONE_NEWUTS | SIGCHLD,argv[1]);

       printf("PID of child created by clone() is %ld\n", (long)child_pid);

    新的子进程将会执行用户定义的函数childFunc();该函数会接收clone()传递的最后一个参数argv[1],由于创建时使用了CLONE_NEWUTS标识,新的UTS命名空间会被创建。

    主程序然后休眠一段时间。这是一个粗暴的方式以让子进程修改UTS命名空间内的主机名。那个程序然后使用uname()获得命名空间内的主机名并且显示主机名。

       sleep(1);           /* Give childtime to change its hostname */

       if (uname(&uts) == -1)

           errExit("uname");

       printf("uts.nodename in child: %s\n", uts.nodename);

    与此同时,子进程执行的函数childFunc()首先更改主机名为命令行输入的参数,然后显示修改后的主机名。

       if (uname(&uts) == -1)

           errExit("uname");

       printf("uts.nodename in child: %s\n", uts.nodename);

    在结束之前,子进程休眠一会。效果就是让子进程的UTS命名空间存在一段时间,这段时间使得我们能够做一些后面我们给出的实验。

    执行如下命令:

       $ su                  # Need privilege to create a UTS namespace

        Password:

        # uname -n

        antero

        # ./demo_uts_namespacesbizarro

        PID of child created byclone() is 27514

        uts.nodename inchild:  bizarro

        uts.nodename in parent:antero

    正如其它命名空间(user namespace除外),创建一个UTS命名空间需要特权权限(CAP_SYS_ADMIN)。这可以避免set-user-ID类的应用程序因系统主机问题被误导而执行错误的事。

    另外一个可能性是set-user-ID类应用可能使用主机名作为锁文件的一部分。如果一个非特权用户能够在一个具有任何主机名的UTS命名空间运行程序,这可能导致应用受到各种攻击。最简单的,这将使锁无效,在不同的UTS命名空间中引发应用实例被运行。另外,一个恶意用户可以在一个UTS命名空间运行一个set-user-ID应用程序来覆盖一个重要文件的锁。

    The /proc/PID/ns 文件

    每一个进程有一个/proc/PID/ns目录,该目录下每一个命名空间对应一个文件。从3.8版本起,每一个这类文件都是一个特殊的符号链接。该符号链接提供在其命名空间上执行某个操作的某种方法。

    $ ls -l/proc/

    /ns         #
    is replaced byshell's PID

        total 0

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 ipc -> ipc:[4026531839]

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 mnt -> mnt:[4026531840]

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 net -> net:[4026531956]

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 pid -> pid:[4026531836]

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 user -> user:[4026531837]

        lrwxrwxrwx. 1 mtk mtk 0Jan  8 04:12 uts -> uts:[4026531838]

    这些符号链接可以用来判断两个命名空间是否在同一个命名空间。如果两个进程在同一个命名空间,内核会保证由/proc/PID/ns导出的inode号将会是一样的。inode号可以通过stat()系统调用获得。

    然而,内核同样为每个 /proc/PID/ns构建了符号链接,以使其指向包含标识命名空间类型字符串(字符串以inode号结尾)的名字。我们可以通过ls -l或者readlink命令查看名字。

    让我们回到上面 demo_uts_namespaces运行的shell会话,通过查看父子进程的/proc/PID/ns符号链接信息可以知道它们是否位于同一个命名空间。

       ^Z                               # Stop parent and child

        [1]+ Stopped         ./demo_uts_namespaces bizarro

        # jobs-l                        # Show PID of parent process

        [1]+ 27513Stopped         ./demo_uts_namespacesbizarro

        # readlink/proc/27513/ns/uts     # Show parent UTS namespace

        uts:[4026531838]

        # readlink/proc/27514/ns/uts     # Show child UTS namespace

        uts:[4026532338]

    正如我们看到的, /proc/PID/ns/uts 符号链接并不一样,表明它们位于不同的命名空间中。

    /proc/PID/ns同样服务于其它目的,如果我们随便打开一个文件,那么只要文件描述打开状态,那么命名空间将会保持存在而不论命名空间中的进程是否全部退出,相同的效果可以通过绑定其中一个符号链接到文件系统的其它地方获得。

     # touch~/uts                           # Create mount point

        # mount --bind/proc/27514/ns/uts ~/uts

    在3.8之前, /proc/PID/ns 下的文件是硬链接,并且只有ipc、net和uts文件是存在的。

    关联一个存在命名空间:setns()

    当一个命名空间没有进程时还保持其打开,这么做是为了后续添加进程到该命名空间。而添加这个功能这就是使用setns()系统调用来完成了,这使得调用的进程能够和命名空间关联:

     intsetns(int fd, int nstype);

    准确来说,setns()将调用的进程和一个特定的命名空间解除关系并将该进程和一个同类型的命名空间相关联。

    fd参数指明了关联的命名空间,其是指向了 /proc/PID/ns 目录下一个符号链接的文件描述符,可以通过打开这些符号链接指向的文件或者打开一个绑定到符号链接的文件来获得文件描述符(所谓的获得指的是引用计数加1)。

    nstype参数运行调用者检查fd指向的命名空间的类型,如果这个参数等于零,将不会检查。当调用者已经知道命名空间的类型时这会很有用。我们的示例程序(ns_exec.c)的nstype参数等于零,其适用于任何命名空间。当nstype被赋值为CLONE_NEW*的常量时,内核会检查fd指向的命名空间的类型。 

    使用setns()和execve()(或者其它的exec()函数)使得我们能够构建一个简单但是有用的工具,一个和特定命名空间关联的程序并且在命名空间中可以执行一个命令。

    /* ns_exec.c

     

      Copyright 2013, Michael Kerrisk

      Licensed under GNU General Public License v2 or later

     

      Join a namespace and execute a command in the namespace

    */

    #define _GNU_SOURCE

    #include <fcntl.h>

    #include <sched.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

     

    /* A simple error-handling function:print an error message based

      on the value in 'errno' and terminate the calling process */

     

    #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                            } while (0)

     

    int

    main(int argc, char *argv[])

    {

       int fd;

     

       if (argc < 3) {

           fprintf(stderr, "%s /proc/PID/ns/FILE cmd [arg...]\n",argv[0]);

           exit(EXIT_FAILURE);

       }

     

       fd = open(argv[1], O_RDONLY);   /*Get descriptor for namespace */

       if (fd == -1)

           errExit("open");

     

       if (setns(fd, 0) == -1)         /*Join that namespace */

           errExit("setns");

     

       execvp(argv[2], &argv[2]);     /* Execute a command in namespace */

       errExit("execvp");

    }

    我们的应用程序接收两个命令行参数。第一个参数是/proc/PID/ns/* 符号链接的路径,剩下的参数是在和第一参数关联的命名空间中将要运行的程序名,程序中的关键步骤如下:

       fd = open(argv[1], O_RDONLY);   /* Get descriptor for namespace */

        setns(fd,0);                  /* Join thatnamespace */

       execvp(argv[2], &argv[2]);      /* Execute acommand in namespace */

    一个在命名空间中执行的有意思的程序当然是shell程序。我么可以使用先前创建的UTS命名空间以及ns_exec程序来在demo_uts_namespaces.c创建的新的UTS命名空间中执行一个shell。

       # ./ns_exec ~/uts /bin/bash     # ~/uts is bound to/proc/27514/ns/uts

       My PID is: 28788

    可以证明新的UTS命名空间创建的shell是emo_uts_namespaces的子进程,可以通过查看主机名或者比较 /proc/PID/ns/uts 的inode节点得到该结论。

       # hostname

       bizarro

       # readlink /proc/27514/ns/uts

       uts:[4026532338]

       # readlink /proc/

    /ns/uts      #
    is replaced byshell's PID

       uts:[4026532338]

    早期的内核版本,使用setns()关联mount、PID和user 命名空间是不可能的,但是从3.8开始支持所有的命名空间类型。

    Leaving a namespace: unshare()

    最后一种命名空间的系统调用是unshare():

       int unshare(int flags);

    unshare()系统调用提供类似clone()的功能,但是作用于调用的进程。其会创建由flags参数中制定的CLONE_NEW*命名空间,并且将调用者作为命名空间的一部分。 unshare()的主要目的是消除命名空间的副作用而不需要创建新的进程或线程。

    撇开clone系统调用的影响,调用的形式是:

     clone(...,CLONE_NEWXXX, ....);

    就命名空间术语来说,等价于下列顺序:

       if (fork() == 0)

           unshare(CLONE_NEWXXX);      /* Executed in the childprocess */

    unshare的系统调用的一个例子是在命令行下使用unshare命令,其允许用户使用shell执行另一个命名空间的命令。该命令的通常形式如下:

       unshare [options] program [arguments]

    参数[arguments] 是传递给命令program的,options传递给unshare指向的命名空间。

    实现unshare命令的关键步骤很直接:

        /* Code to initialize 'flags' according to command-line options

           omitted */

        unshare(flags);

         /* Now execute 'program' with 'arguments'; 'optind' is the index

           of the next command-line argument after options */

         execvp(argv[optind], &argv[optind]);

    一个简单的unshare命令的实现代码如下:

    /* unshare.c 

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

       A simple implementation of the unshare(1)command: unshare

       namespaces and execute a command.

    */

     

    #define_GNU_SOURCE

    #include<sched.h>

    #include<unistd.h>

    #include<stdlib.h>

    #include<stdio.h>

     

    /* A simpleerror-handling function: print an error message based

       on the value in 'errno' and terminate thecalling process */

     

    #defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                            } while (0)

     

    static void

    usage(char *pname)

    {

        fprintf(stderr, "Usage: %s [options]program [arg...]\n", pname);

        fprintf(stderr, "Options canbe:\n");

        fprintf(stderr, "    -i  unshare IPC namespace\n");

        fprintf(stderr, "    -m  unshare mount namespace\n");

        fprintf(stderr, "    -n  unshare network namespace\n");

        fprintf(stderr, "    -p  unshare PID namespace\n");

        fprintf(stderr, "    -u  unshare UTS namespace\n");

        fprintf(stderr, "    -U  unshare user namespace\n");

        exit(EXIT_FAILURE);

    }

     

    int

    main(int argc,char *argv[])

    {

        int flags, opt;

        flags = 0;

     

        while ((opt = getopt(argc, argv,"imnpuU")) != -1) {

            switch (opt) {

            case 'i': flags |= CLONE_NEWIPC;        break;

            case 'm': flags |= CLONE_NEWNS;         break;

            case 'n': flags |= CLONE_NEWNET;        break;

            case 'p': flags |= CLONE_NEWPID;        break;

            case 'u': flags |= CLONE_NEWUTS;        break;

            case 'U': flags |= CLONE_NEWUSER;       break;

            default:  usage(argv[0]);

            }

        }

        if (optind >= argc)

            usage(argv[0]);

        if (unshare(flags) == -1)

            errExit("unshare");

        execvp(argv[optind],&argv[optind]); 

        errExit("execvp");

    }

    在下面的shell会话中,我们使用unshare.c程序在另外一个mount命名空间中执行一个shell。

       # echo$$                            # Show PID of shell

       8490

       # cat /proc/8490/mounts | grep mq     # Show one of themounts in namespace

       mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0

       # readlink/proc/8490/ns/mnt          # Showmount namespace ID

        mnt:[4026531840]

       # ./unshare -m/bin/bash             # Start new shell in separate mount namespace

       # readlink/proc/$$/ns/mnt           # Show mount namespace ID

        mnt:[4026532325]

    从上述readlink输出可以看到两个shell属于不同的命名空间,改变一个命名空间的挂载点,然后查看另外一个命名空间对上述改变是否可见即可分辨它们是否位于同一个命名空间。

       # umount /dev/mqueue                 # Remove a mount point in this shell

       # cat /proc/$$/mounts | grep mq       # Verifythat mount point is gone

       # cat /proc/8490/mounts | grep mq     # Is it still presentin the other namespace?

     mqueue/dev/mqueue mqueuerw,seclabel,relatime 0 0

    从输出的最后两个参数看到, /dev/mqueue挂载点一个命名空间可以看到而另一个命名空间看不到。

    总结

    本文我们查看了命名空间API以及它们的使用。接下来的文章,我们将更深入查看命名空间的,特别会深入查看PID和user 命名空间。

    PID命名空间

    被PID命名空间隔离的全局资源是进程ID号空间,这就意味着位于不同命名空间的进程的ID号可以相同,PID命名空间被用来实现container。

    和传统Linux系统一样,在PID命名空间内的进程ID号是各不相等的,它们被从1开始分配进程ID号。同样的,ID号等于1的init进程是一个特殊进程,它是命名空间中的第一个进程,它也命名空间提供一些管理工作。

    初探

    一个新的PID命名空间调用clone(...CLONE_NEWPID...)创建,我们将展示一个简单的使用clone创建PID命名空间的例子,并且使用该例子阐释PID命名空间的基本概念,完整的pidns_init_sleep.c的源码如下:

    /*pidns_init_sleep.c

     

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

     

       A simple demonstration of PID namespaces.

    */

    #define_GNU_SOURCE

    #include<sched.h>

    #include<unistd.h>

    #include<stdlib.h>

    #include<sys/wait.h>

    #include<sys/mount.h>

    #include<sys/types.h>

    #include<sys/stat.h>

    #include<string.h>

    #include<signal.h>

    #include<stdio.h>

     

    /* A simpleerror-handling function: print an error message based

       on the value in 'errno' and terminate thecalling process */

     

    #defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                            } while (0)

     

    static int              /* Start function for clonedchild */

    childFunc(void*arg)

    {

        printf("childFunc(): PID  = %ld\n", (long) getpid());

        printf("childFunc(): PPID =%ld\n", (long) getppid());

     

        char *mount_point = arg;

     

        if (mount_point != NULL) {

            mkdir(mount_point, 0555);       /* Create directory for mount point */

            if (mount("proc",mount_point, "proc", 0, NULL) == -1)

                errExit("mount");

            printf("Mounting procfs at %s\n",mount_point);

        }

     

        execlp("sleep","sleep", "600", (char *) NULL);

        errExit("execlp");  /* Only reached if execlp() fails */

    }

     

    #define STACK_SIZE(1024 * 1024)

     

    static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

     

    int

    main(int argc,char *argv[])

    {

        pid_t child_pid;

     

        child_pid = clone(childFunc,

                        child_stack +STACK_SIZE,   /* Points to start of

                                                      downwardly growing stack */

                        CLONE_NEWPID | SIGCHLD,argv[1]);

     

        if (child_pid == -1)

            errExit("clone");

     

        printf("PID returned by clone():%ld\n", (long) child_pid);

     

        if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */

            errExit("waitpid");

     

        exit(EXIT_SUCCESS);

    }

    主程序使用clone()创建一个PID命名空间,并且显示了返回的PID号。

     child_pid =clone(childFunc,

                       child_stack +STACK_SIZE,   /* Points to start of

                                                      downwardly growing stack */

                       CLONE_NEWPID | SIGCHLD, argv[1]);

        printf("PID returned by clone(): %ld\n", (long) child_pid);

    新的子进程开始执行childFunc()函数,该函数接收clone()调用的最后一个参数argv[1],该参数的作用后续讲解。 childFunc()函数显示进程和其父进程ID,并且使用sleep函数结束。

       printf("childFunc(): PID = %ld\n", (long) getpid());

       printf("ChildFunc(): PPID = %ld\n", (long) getppid());

        ...

       execlp("sleep", "sleep", "1000", (char *) NULL);

    使用sleep的主要价值在于其让我们区分子进程和父进程更简单。当我们允许该程序, 第一行的输出如下:

       $ su         # Need privilege to createa PID namespace

       Password:

        #./pidns_init_sleep /proc2

       PID returned by clone(): 27656

       childFunc(): PID  = 1

       childFunc(): PPID = 0

       Mounting procfs at /proc2

     pidns_init_sleep 前两行输出了从两个不同的PID命名空间查看子进程的ID号:clone()调用所在的PID命名空间和子进程存在的PID命名空间。即子进程有两个PID:父进程命名空间中的为27656,新创建的命名空间中的ID是1。输出的下一行展示了子进程的父进程ID。父进程ID等于0显示了命名空间实现的一点怪异之处。正如下面我们讨论的,命名空间形成一个层次结构:一个进程仅仅能够"看见"在其命名空间内和子命名空间内的进程ID(子进程的子进程...均能看见),子进程看不见父进程命名空间的进程。由于由clone()创建的子进程同父进程在不同的命名空间中,所有子进程“看”不到父进程;因此,getppid()返回的父进程ID为0.

    为了解释最后一行,我们需要重新查看childFunc() 中跳过的一些代码段。

    /proc/PID和PID命名空间

    Linux系统上的每一个进程都有一个/proc/PID目录,该目录包括了描述进程的伪文件。这一机制可以直接得到PID的命名空间。在一个命名空间内部,/proc/PID目录仅包含在该命名空间内和子命名空间。

    然而,为了使和一个PID命名空间相关的/proc/PID目录可见,proc文件系统("procfs")需要在PID命名空间内被挂载。在一个命名空间内运行的shell上,我们可以使用mount命令完成:

     #mount -tproc proc/mount_point

    也可以使用mount()系统调用完成,这里在childFunc()里调用如下:

       mkdir(mount_point, 0555);       /* Createdirectory for mount point */

       mount("proc", mount_point, "proc", 0, NULL);

       printf("Mounting procfs at %s\n", mount_point);

    mount_pioint参数在运行pidns_init_sleep时通过命令行参数给出。

    在我们的例子中,shell中运行pidns_init_sleep,我们在/proc2目录下挂着procfs。在现实世界中,procfs通常被挂载在/proc目录下。然而,我们的例子在/proc2目录下挂载procfs,这避免了会给系统上其它进程带来问题:因为他们位于同样的挂载点,更改挂载在/proc目录下的文件系统将使得root PID 命名空间“看”不到/proc/PID目录。

    所以,在我们的shell会话中,/proc目录下挂载的procfs将显示从父PID命名空间能够看到的进程的PID子目录,/proc2则用于子进程命名空间。需要提醒的是虽然子进程PID命名空间的进程能够看见由/proc挂载点导出的PID目录,但是这些PID目录是对于子进程PID命名空间的进程而言是无意义的,因为这些进程的系统调用只能看到它们所在命名空间的PID。

    如果我们想让像ps这样的工具在一个子进程中能够正确运行,那么在/proc挂载点挂载一个procfs文件系统还是必要的。因为这些工具的信息源于/proc目录。有两种方法在不影响父进程使用的PID命名空间前提下达到这个目标。其一,如果子进程使用CLONE_NEWNS标志创建,那么子进程将和系统的其它部分在不同的mount 命名空间,在这种情况下,在/proc目录下挂载procfs不会产生任何问题。另外,不采用CLONE_NEWNS的方法,子进程可以使用chroot()并在/proc目录下挂载procfs。

    让我们回到运行pidns_init_sleep程序的shell上,我们停止该程序并在fu2命名空间中使用ps检查父子进程的一些信息。

       ^Z                         Stopthe program, placing in background

       [1]+ Stopped                ./pidns_init_sleep /proc2

       # ps -C sleep -C pidns_init_sleep -o "pid ppid stat cmd"

         PID  PPID STAT CMD

       27655 27090 T    ./pidns_init_sleep /proc2

       27656 27655 S    sleep 600

    PPID的值为27655,最后一行系显示了sleep是在父进程中执行的。

    通过使用readlink命令查看父子进程的不同/proc/PID/ns/pid符号链接信息,我们可以看到两个进程在不同的PID命名空间中:

       # readlink /proc/27655/ns/pid

       pid:[4026531836]

       # readlink /proc/27656/ns/pid

       pid:[4026532412]

    到此,我们可以使用新挂载的procfs获得新PID命名空间的进程信息。我们可以使用下面的命令获得PID的列表:

       # ls -d /proc2/[1-9]*

       /proc2/1

    正如我们看到的,PID命名空间仅仅包括一个进程,它的进程ID号是1。同样也可以使用/proc/PID/status文件作为一个获得一个进程信息的不同方法。

       # cat /proc2/1/status | egrep '^(Name|PP*id)'

       Name:   sleep

       Pid:    1

       PPid:   0

    PPid是0,符合前面getppid()系统调用的返回的父进程的ID号。

    嵌套的 PID 命名空间

    如前面提到的,PID命名空间是以父子关系层次嵌套的。在一个PID命名空间内,可以看同一个命名空间中的所有其它进程以及后裔进程。这里,“看见”意思是能够在特定的进程ID号上使用系统调用,一个子PID命名空间不能看见父PPID的命名空间。

    一个进程在PID命名空间的每一层都有一个进程ID,存在的范围是该进程的PID命名空间一直到root命名空间。getpid()总是返回PID命名空间内的进程ID。

    我们可以使用multi_pidns.c来展示一个进程在不同的命名空间中拥有不同的进程ID号。为了简洁,我们简单阐述该程序都做了什么。

    /* multi_pidns.c

     

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

     

       Create a series of child processes in nestedPID namespaces.

    */

    #define_GNU_SOURCE

    #include<sched.h>

    #include<unistd.h>

    #include<stdlib.h>

    #include<sys/wait.h>

    #include<string.h>

    #include<signal.h>

    #include<stdio.h>

    #include<limits.h>

    #include<sys/mount.h>

    #include<sys/types.h>

    #include<sys/stat.h>

     

    /* A simpleerror-handling function: print an error message based

       on the value in 'errno' and terminate thecalling process */

     

    #defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                            } while (0)

     

    #define STACK_SIZE(1024 * 1024)

     

    static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

                    /* Since each child gets a copyof virtual memory, this

                       buffer can be reused as eachchild creates its child */

     

    /* Recursivelycreate a series of child process in nested PID namespaces.

       'arg' is an integer that counts down to 0during the recursion.

       When the counter reaches 0, recursion stopsand the tail child

       executes the sleep(1) program. */

     

    static int

    childFunc(void*arg)

    {

        static int first_call = 1;

        long level = (long) arg;

     

        if (!first_call) {

     

            /* Unless this is the first recursivecall to childFunc()

               (i.e., we were invoked from main()),mount a procfs

               for the current PID namespace */

     

            char mount_point[PATH_MAX];

     

            snprintf(mount_point, PATH_MAX,"/proc%c", (char) ('0' + level));

     

            mkdir(mount_point, 0555);       /* Create directory for mount point */

            if (mount("proc",mount_point, "proc", 0, NULL) == -1)

                errExit("mount");

            printf("Mounting procfs at %s\n",mount_point);

        }

     

        first_call = 0;

     

        if (level > 0) {

     

            /* Recursively invoke childFunc() tocreate another child in a

               nested PID namespace */

     

            level--;

            pid_t child_pid;

     

            child_pid = clone(childFunc,

                        child_stack +STACK_SIZE,   /* Points to start of

                                                      downwardly growing stack */

                        CLONE_NEWPID | SIGCHLD,(void *) level);

     

            if (child_pid == -1)

                errExit("clone");

     

            if (waitpid(child_pid, NULL, 0) ==-1)  /* Wait for child */

                errExit("waitpid");

     

        } else {

     

            /* Tail end of recursion: executesleep(1) */

     

            printf("Final childsleeping\n");

            execlp("sleep","sleep", "1000", (char *) NULL);

            errExit("execlp");

        }

     

        return 0;

    }

     

    int

    main(int argc,char *argv[])

    {

        long levels;

     

        levels = (argc > 1) ? atoi(argv[1]) : 5;

        childFunc((void *) levels);

     

        exit(EXIT_SUCCESS);

    }

    该程序递归创建一系列的子进程命名空间,命令行参数指明了子进程和PID命名空间的创建次数:

    #./multi_pidns5

    除了递归创建子进程,每一个递归步骤还会在一个独一无二的挂载点挂载procfs文件系统。递归最后的子进程执行sleep()系统调用。上面的命令行产生如下的输出:

       Mounting procfs at /proc4

       Mounting procfs at /proc3

       Mounting procfs at /proc2

       Mounting procfs at /proc1

       Mounting procfs at /proc0

       Final child sleeping

    在每一个procfs下查看PID,我们看到越是后创建的procfs包含的PID越少,表明每一个PID命名空间只显示其命名空间自身以及其后创建的命名空间的信息。

       ^Z                          Stop the program, placing in background

       [1]+ Stopped            ./multi_pidns5

       # ls -d /proc4/[1-9]*        Topmost PIDnamespace created by program

       /proc4/1  /proc4/2  /proc4/3  /proc4/4  /proc4/5

       # ls -d /proc3/[1-9]*

       /proc3/1  /proc3/2  /proc3/3  /proc3/4

       # ls -d /proc2/[1-9]*

       /proc2/1  /proc2/2  /proc2/3

       # ls -d /proc1/[1-9]*

       /proc1/1  /proc1/2

       # ls -d /proc0/[1-9]*        Bottommost PIDnamespace

       /proc0/1

    一个grep命令使得我们能够看见递归最后的PID。

       # grep -H 'Name:.*sleep' /proc?/[1-9]*/status

       /proc0/1/status:Name:       sleep

       /proc1/2/status:Name:       sleep

       /proc2/3/status:Name:       sleep

       /proc3/4/status:Name:       sleep

       /proc4/5/status:Name:       sleep

    换句话说,嵌套最深的PID命名空间(/proc0),该进程执行sleep并且进程ID是1,在最上面创建的PID命名空间是/proc4,进程的PID是5。

    如果你运行本文的例子,需要说明的是它们将残留下挂载点和挂载目录。在结束程序时,如下的shell命令行完成做够的清理工作:

       # umount /proc?

       # rmdir /proc?

    总结

    在本文,我们了解了一些PID命名空间的操作。在下文中,我们将讨论PID命名空间的init进程和一些其它的PID命名空间的API。

     

    深入PID 命名空间

    本文是对PID命名空间的更深入探讨。PID命名空间的一个应用是打包一组进程(container),使打包的进程组自身就像一个操作系统。传统操作系统和这里的container一样的一个关键点是init进程。所以我们来看看init进程以及两种情况下都有哪些不同。按惯例,我们将看看适用于PID命名空间的其它一些细节。

    PID命名空间的init进程

    在PID命名空间中创建的第一个进程的进程ID是1,该进程的角色和传统操作系统的init进程一样;特别地,init进程能够完成PID命名空间需要的初始化工作(这些工作很可能包括启动其它进程),其同样会是孤儿进程的父进程。

    为了解释PID命名空间,我们将使用一些服务于目的的例程。第一个例程是ns_child_exec.c,命令行运行的语法如下:

    ns_child_exec[options]command [arguments]

    /* ns_child_exec.c

     

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

     

       Create a child process that executes a shellcommand in new namespace(s).

    */

    #define_GNU_SOURCE

    #include <sched.h>

    #include<unistd.h>

    #include<stdlib.h>

    #include<sys/wait.h>

    #include<signal.h>

    #include<stdio.h>

     

    /* A simpleerror-handling function: print an error message based

       on the value in 'errno' and terminate thecalling process */

     

    #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                            } while (0)

     

    static void

    usage(char *pname)

    {

        fprintf(stderr, "Usage: %s [options]cmd [arg...]\n", pname);

        fprintf(stderr, "Options canbe:\n");

        fprintf(stderr, "    -i  new IPC namespace\n");

        fprintf(stderr, "    -m  new mount namespace\n");

        fprintf(stderr, "    -n  new network namespace\n");

        fprintf(stderr, "    -p  new PID namespace\n");

        fprintf(stderr, "    -u  new UTS namespace\n");

        fprintf(stderr, "    -U  new user namespace\n");

        fprintf(stderr, "    -v  Display verbose messages\n");

        exit(EXIT_FAILURE);

    }

     

    static int              /* Start function for clonedchild */

    childFunc(void*arg)

    {

        char **argv = arg;

     

        execvp(argv[0], &argv[0]);

        errExit("execvp");

    }

     

    #define STACK_SIZE(1024 * 1024)

     

    static charchild_stack[STACK_SIZE];    /* Space forchild's stack */

     

    int

    main(int argc,char *argv[])

    {

        int flags, opt, verbose;

        pid_t child_pid;

     

        flags = 0;

        verbose = 0;

     

        /* Parse command-line options. The initial'+' character in

           the final getopt() argument preventsGNU-style permutation

           of command-line options. That's useful,since sometimes

           the 'command' to be executed by thisprogram itself

           has command-line options. We don't wantgetopt() to treat

           those as options to this program. */

     

        while ((opt = getopt(argc, argv,"+imnpuUv")) != -1) {

            switch (opt) {

            case 'i': flags |= CLONE_NEWIPC;        break;

            case 'm': flags |= CLONE_NEWNS;         break;

            case 'n': flags |= CLONE_NEWNET;        break;

            case 'p': flags |= CLONE_NEWPID;        break;

            case 'u': flags |= CLONE_NEWUTS;        break;

            case 'U': flags |= CLONE_NEWUSER;       break;

            case 'v': verbose = 1;                  break;

            default:  usage(argv[0]);

            }

        }

     

        child_pid = clone(childFunc,

                        child_stack + STACK_SIZE,

                        flags | SIGCHLD,&argv[optind]);

        if (child_pid == -1)

            errExit("clone");

     

        if (verbose)

            printf("%s: PID of child createdby clone() is %ld\n",

                    argv[0], (long) child_pid);

     

        /* Parent falls through to here */

     

        if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */

            errExit("waitpid");

     

        if (verbose)

            printf("%s: terminating\n",argv[0]);

        exit(EXIT_SUCCESS);

    }

    ns_child_exec程序使用clone()系统调用来创建子进程;子进程然后执行命令行中的command命令,命令的参数是命令行中的argument参数。命令行中的option参数用于指定要创建的命名空间类型,该参数将传递给clone()系统调用。例如 -p选项将指导子进程创建新的PID命名空间,如下所示:

       $ su                 # Need privilege to create aPID namespace

       Password:

       # ./ns_child_exec -p sh -c 'echo $$'

       1

    上面的命令行在新的PID命名空间中创建了一个子进程,该子进程执行一个显示shell进程ID的echo命令。该shell进程ID是1,当shell运行时其将是PID命名空间的init进程。

    我们的下一个例程,simple_init.c是一个我们要将其作为一个PID命名空间的init进程的程序。该程序用于证实PID命名空间的init进程的一些特性。

    /* simple_init.c

     

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

     

       A simple init(1)-style program to be used asthe init program in

       a PID namespace. The program reaps thestatus of its children and

       provides a simple shell facility forexecuting commands.

    */

    #define_GNU_SOURCE

    #include<unistd.h>

    #include<stdio.h>

    #include<stdlib.h>

    #include<string.h>

    #include<signal.h>

    #include<wordexp.h>

    #include<errno.h>

    #include<sys/wait.h>

     

    #defineerrExit(msg)    do { perror(msg);exit(EXIT_FAILURE); \

                            } while (0)

     

    static int verbose= 0;

     

    /* Display waitstatus (from waitpid() or similar) given in 'status' */

     

    /* SIGCHLDhandler: reap child processes as they change state */

     

    static void

    child_handler(intsig)

    {

        pid_t pid;

        int status;

     

        /* WUNTRACED and WCONTINUED allow waitpid()to catch stopped and

           continued children (in addition toterminated children) */

     

        while ((pid = waitpid(-1, &status,

                              WNOHANG | WUNTRACED |WCONTINUED)) != 0) {

            if (pid == -1) {

                if (errno == ECHILD)        /* No more children */

                    break;

                else

                   perror("waitpid");     /* Unexpected error */

            }

     

            if (verbose)

                printf("\tinit: SIGCHLDhandler: PID %ld terminated\n",

                        (long) pid);

        }

    }

     

    /* Perform wordexpansion on string in 'cmd', allocating and

       returning a vector of words on success orNULL on failure */

     

    static char **

    expand_words(char*cmd)

    {

        char **arg_vec;

        int s;

        wordexp_t pwordexp;

     

        s = wordexp(cmd, &pwordexp, 0);

        if (s != 0) {

            fprintf(stderr, "Word expansionfailed\n");

            return NULL;

        }

     

        arg_vec = calloc(pwordexp.we_wordc + 1,sizeof(char *));

        if (arg_vec == NULL)

            errExit("calloc");

     

        for (s = 0; s < pwordexp.we_wordc; s++)

            arg_vec[s] = pwordexp.we_wordv[s];

     

        arg_vec[pwordexp.we_wordc] = NULL;

     

        return arg_vec;

    }

     

    static void

    usage(char *pname)

    {

        fprintf(stderr, "Usage: %s[-q]\n", pname);

        fprintf(stderr, "\t-v\tProvide verboselogging\n");

     

        exit(EXIT_FAILURE);

    }

     

    int

    main(int argc,char *argv[])

    {

        struct sigaction sa;

    #define CMD_SIZE10000

        char cmd[CMD_SIZE];

        pid_t pid;

        int opt;

     

        while ((opt = getopt(argc, argv,"v")) != -1) {

            switch (opt) {

            case 'v': verbose = 1;          break;

            default:  usage(argv[0]);

            }

        }

     

        sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;

        sigemptyset(&sa.sa_mask);

        sa.sa_handler = child_handler;

        if (sigaction(SIGCHLD, &sa, NULL) ==-1)

            errExit("sigaction");

     

        if (verbose)

            printf("\tinit: my PID is%ld\n", (long) getpid());

     

        /* Performing terminal operations while notbeing the foreground

           process group for the terminal generatesa SIGTTOU that stops the

           process. However our init "shell" needs to be able to perform

           such operations (just like a normalshell), so we ignore that

           signal, which allows the operations toproceed successfully. */

     

        signal(SIGTTOU, SIG_IGN);

     

        /* Become leader of a new process group andmake that process

           group the foreground process group forthe terminal */

     

        if (setpgid(0, 0) == -1)

            errExit("setpgid");;

        if (tcsetpgrp(STDIN_FILENO, getpgrp()) ==-1)

            errExit("tcsetpgrp-child");

     

        while (1) {

     

            /* Read a shell command; exit on end offile */

     

            printf("init$ ");

            if (fgets(cmd, CMD_SIZE, stdin) ==NULL) {

                if (verbose)

                    printf("\tinit:exiting");

                printf("\n");

                exit(EXIT_SUCCESS);

            }

     

            if (cmd[strlen(cmd) - 1] == '\n')

                cmd[strlen(cmd) - 1] = '\0';        /* Strip trailing '\n' */

     

            if (strlen(cmd) == 0)

                continue;           /* Ignore empty commands */

     

            pid = fork();           /* Create child process */

            if (pid == -1)

                errExit("fork");

     

            if (pid == 0) {         /* Child */

                char **arg_vec;

     

                arg_vec = expand_words(cmd);

                if (arg_vec == NULL)        /* Word expansion failed */

                    continue;

     

                /* Make child the leader of a newprocess group and

                   make that process group theforeground process

                   group for the terminal */

     

                if (setpgid(0, 0) == -1)

                    errExit("setpgid");;

                if (tcsetpgrp(STDIN_FILENO,getpgrp()) == -1)

                   errExit("tcsetpgrp-child");

     

                /* Child executes shell command andterminates */

     

                execvp(arg_vec[0], arg_vec);

                errExit("execvp");          /* Only reached if execvp() fails */

            }

     

            /* Parent falls through to here */

     

            if (verbose)

                printf("\tinit: created child%ld\n", (long) pid);

     

            pause();                /* Will be interrupted by signalhandler */

     

            /* After child changes state, ensurethat the 'init' program

               is the foreground process group forthe terminal */

     

            if (tcsetpgrp(STDIN_FILENO, getpgrp())== -1)

                errExit("tcsetpgrp-parent");

        }

    }

    simple_init程序实现init进程的两个主要功能,一个是系统初始化,大多数init程序是复杂的且多半是基于表的方法来初始化系统。我们的simple_init程序提供一个简单的shell工具,该工具使我们能够手动执行需要初始化命名空间的任何命令;该方法使我们能够随意执行shell命令以对命名空间做一些测试。另一个simple_init实现的功能是使用waitpid()获得其子进程的退出状态。

    所以可以联合使用ns_child_exec和simple_init来在一个新的PID命名空间中启动init进程:

       # ./ns_child_exec -p ./simple_init

       init$

    init$提示符表明simple_init程序能够读取和执行shell命令。

    我们将使用上面的两个以及下面的orphan.c示例来证实在PID命名空间中变为孤儿进程将被PID命名空间中的init进程收留,而不是系统的init进程收留。

    /* orphan.c

     

       Copyright 2013, Michael Kerrisk

       Licensed under GNU General Public License v2or later

     

       Demonstrate that a child becomes orphaned(and is adopted by init(1),

       whose PID is 1) when its parent exits.

    */

    #include<stdio.h>

    #include<stdlib.h>

    #include<unistd.h>

     

    int

    main(int argc,char *argv[])

    {

        pid_t pid;

     

        pid = fork();

        if (pid == -1) {

            perror("fork");

            exit(EXIT_FAILURE);

        }

     

        if (pid != 0) {             /* Parent */

            printf("Parent (PID=%ld) createdchild with PID %ld\n",

                    (long) getpid(), (long) pid);

            printf("Parent (PID=%ld; PPID=%ld)terminating\n",

                    (long) getpid(), (long)getppid());

            exit(EXIT_SUCCESS);

        }

     

        /* Child falls through to here */

     

        do {

            usleep(100000);

        }while (getppid() != 1);           /* Am Ian orphan yet? */

     

        printf("\nChild  (PID=%ld) now an orphan (parentPID=%ld)\n",

                (long) getpid(), (long) getppid());

     

        sleep(1);

     

        printf("Child  (PID=%ld) terminating\n", (long) getpid());

        _exit(EXIT_SUCCESS);

    }

    orphan程序执行一个fork()命令创建子进程。在进程继续执行时父进程会退出;当父进程退出时子进程变成孤儿进程。子进程执行一个循环直到其变为一个孤儿进程(getppid()的返回值是1);一旦子进程变成孤儿进程,它将退出。父子进程打印的信息使我们能够看见当两个子进程退出时刻以及何时子进程变成孤儿进程。

    为了更清楚simple_init程序接受孤儿进程的哪些信息,我们使用-v选项,该选项将产生子进程自己产生的各种信息。

        # ./ns_child_exec-p ./simple_init -v

               init: my PID is 1

       init$ ./orphan

               init: created child 2

       Parent (PID=2) created child with PID 3

       Parent (PID=2; PPID=1) terminating

               init: SIGCHLD handler: PID 2terminated

       init$                   #simple_init promptinterleaved with output from child

       Child (PID=3) now an orphan (parent PID=1)

       Child (PID=3) terminating

               init: SIGCHLD handler: PID 3terminated

    上面的输出中,以init:开始的信息由simple_init在启动verbose选项时打印出的。其它所有的信息(除了init$提示符)由orphan程序打印。从输出来看,子进程(PID是3)在父进程(PID是2)退出时变成孤儿进程。变成孤儿进程的子进程(PID是3)被命名空间的init进程(PID是1)收留。

    信号和init进程

    传统的init进程对信号的处理有些特殊。能够发送给init进程的信号是那些已经有信号处理函数的信号。其它所有信号将被忽略。这阻止了init进程被任何用户意外的kill掉,init进程的存在对于系统的稳定是至关重要的。

    PID命名空间使命名空间内的init进程实现了和传统init类似的一些行为。命名空间内的其它进程(即使特权进程)仅能够发送init进程建立了处理函数的信号。这防止了命名空间内的其它进程不经意地杀死了命名空间中具有特殊作用的init进程。然而,如同传统的init进程一样,在通常的环境中内核仍然能够为PID命名空间内的init进程产生信号(例如,硬件中断,终端产生的SIGTOU等信号,定时器超时)。

    PID命名空间内的祖先进程仍然能够发送信号给子PID命名空间内的Init进程。同样的,只有init设置了对应处理函数的信号才可以被发送给它,除了SIGKILL和SIGSTOP这两个特例。当一个位于祖先PID命名空间的进程发送上述两个特殊信号给init进程时,它们被强行发送。SIGSTOP停止init进程;SIGKILL终结init进程。由于init进程对于PID命名空间如此重要,以至于如果init进程被SIGKILL信号终结掉,内核将对该PID命名空间内的其它所有进程发送SIGKILL信号来终结所有进程。

    通常,PID命名空间在init进程终结时将被摧毁,然而,特例是:只要该命名空间内对应的/proc/PID/ns/pid文件被打开或者绑定挂载点,命名空间将不会被摧毁。然而,不太可能使用setns()和fork()在新的命名空间中创建进程:在fork()调用时会检测到缺少init进程,这将会返回ENOMEM错误码(就是PID不能够被分配)。换句话说,PID命名空间仍然存在,但是变得不可用。

    挂载一个procfs文件系统

    在先前这个系列的文章中,PID命名空间的/proc文件系统procfs被挂载在不同的挂载点(而不是在/proc挂载点),这使得我们可以使用shell命令行载对应的/proc/PID目录下查看每一个新的PID命名空间,同时也可以使用ps命令查看在rootPID命名空间可以看见的进程。

    然而像ps之类内容依赖于挂载在/proc目录下的procfs文件以获取它们需要的信息。因此,如果我们想让ps在命名空间中也运行正确,我们需要为命名空间挂载一个procfs。由于simple_init程序允许我们执行shell命令行,我们可以再命令行下使用如下mount命令:

       # ./ns_child_exec -p -m ./simple_init

       init$ mount -t proc proc /proc

       init$ ps a

         PID TTY      STAT  TIME COMMAND

           1 pts/8    S     0:00 ./simple_init

           3 pts/8    R+    0:00 ps a

    ps命令列出了通过/proc命令能够存取的所有进程。在这种情况下,我们仅看见两个进程,表明命名空间只有两个进程在运行。

    当运行上面的ns_child_exec命令,我们使用了了-m参数,该参数用于将创建的子进程放在一个独立的mount命名空间。这使得mount命令并不会影响命名空间之外的进程看到的/proc挂载点。

    unshare()和setns()

    自从3.8版本,上面两个系统调用能够被PID命名空间使用,但是有些特殊的地方。

    指定CLONE_NEWPID标志使用unshare()创建新的PID命名空间,但是并没有将unshare的调用者放入命名空间中。而调用者创建的任何子进程将被放在新的命名空间中;第一个子进程将是该命名空间的init进程。

    setns()系统调用现在支持PID命名空间:

       setns(fd, 0);   /* Second argument can be CLONE_NEWPIDtoforce a

                          check that 'fd' refersto a PID namespace */

    fd参数是一个指明PID命名空间的文件描述符,该描述符是PID命名空间调用者的后裔;通过打开目标命名空间中一个进程的/proc/PID/ns/pid文件来获得该文件描述符。unshare()、setns()不将调用者放在创建的命名空间中,而调用者后续创建的子进程将被放入命名空间中。

    我们可以使用一个增强型ns_exec.c版本来验证setns()的一些特性。

    /* ns_exec.c

     

      Copyright 2013, Michael Kerrisk

      Licensed under GNU General Public License v2 or later

     

      Join a namespace and execute a command in the namespace

    */

    #define _GNU_SOURCE

    #include <fcntl.h>

    #include <sched.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

     

    /* A simple error-handling function: printan error message based

      on the value in 'errno' and terminate the calling process */

     

    #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                            } while (0)

     

    int

    main(int argc, char *argv[])

    {

       int fd;

     

       if (argc < 3) {

           fprintf(stderr, "%s /proc/PID/ns/FILE cmd [arg...]\n",argv[0]);

           exit(EXIT_FAILURE);

       }

     

       fd = open(argv[1], O_RDONLY);   /*Get descriptor for namespace */

       if (fd == -1)

           errExit("open");

     

       if (setns(fd, 0) == -1)         /*Join that namespace */

           errExit("setns");

     

       execvp(argv[2], &argv[2]);     /* Execute a command in namespace */

       errExit("execvp");

    }

    新的ns_run.c程序如下:

    /* ns_run.c

     

      Copyright 2013, Michael Kerrisk

      Licensed under GNU General Public License v2 or later

     

      Join one or more namespaces using setns() and execute a command in

      those namespaces, possibly inside a child process.

     

      This program is similar in concept to nsenter(1), but has a

      different command-line interface.

    */

    #define _GNU_SOURCE

    #include <fcntl.h>

    #include <sched.h>

    #include <unistd.h>

    #include <stdlib.h>

    #include <stdio.h>

    #include <sys/wait.h>

     

    /* A simple error-handling function: printan error message based

      on the value in 'errno' and terminate the calling process */

     

    #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

                            } while (0)

     

    static void

    usage(char *pname)

    {

       fprintf(stderr, "Usage: %s [-f] [-n /proc/PID/ns/FILE] cmd[arg...]\n",

               pname);

       fprintf(stderr, "\t-f    Execute command in child process\n");

       fprintf(stderr, "\t-n    Join specified namespace\n");

     

       exit(EXIT_FAILURE);

    }

     

    int

    main(int argc, char *argv[])

    {

       int fd, opt, do_fork;

       pid_t pid;

     

       /* Parse command-line options. The initial '+' character in

          the final getopt() argument prevents GNU-style permutation

          of command-line options. That's useful, since sometimes

          the 'command' to be executed by this program itself

          has command-line options. We don't want getopt() to treat

          those as options to this program. */

     

       do_fork = 0;

       while ((opt = getopt(argc, argv, "+fn:")) != -1) {

           switch (opt) {

     

           case 'n':       /* Join anamespace */

               fd = open(optarg, O_RDONLY); /* Get descriptor for namespace */

               if (fd == -1)

                    errExit("open");

     

               if (setns(fd, 0) == -1)      /* Join that namespace */

                    errExit("setns");

               break;

     

           case 'f':

               do_fork = 1;

               break;

     

           default:

               usage(argv[0]);

           }

       }

     

       if (argc <= optind)

           usage(argv[0]);

     

       /* If the "-f" option was specified, execute the suppliedcommand

          in a child process. This is mainly useful when working with PID

          namespaces, since setns() to a PID namespace only places

          (subsequently created) child processes in the names, and

          does not affect the PID namespace membership of the caller. */

     

       if (do_fork) {

           pid = fork();

           if (pid == -1)

               errExit("fork");

     

           if (pid != 0) {                 /*Parent */

               if (waitpid(-1, NULL, 0) == -1)    /* Wait for child */

                    errExit("waitpid");

               exit(EXIT_SUCCESS);

           }

     

           /* Child falls through to code below */

       }

     

       execvp(argv[optind], &argv[optind]);

        errExit("execvp");

    }

    ns_run [-f][-n/proc/PID/ns/FILE]... command [arguments]

    该程序使用setns()关联由/proc/PID/ns(-n选项指定)文件指定的命名空间。然后其执行给定的command命令并将参数arguments传递给它,如果指定了-f参数,它使用fork()创建的子进程执行上面的命令。

    考虑一个场景,在一个终端窗口,我们在一个新的PID命名空间中启动simple_init程序,启动verbose选项记录日志以方便我们知道何时子进程被收留:

       # ./ns_child_exec -p ./simple_init -v

               init: my PID is 1

       init$ 

    然后我们切到第二个终端,这里我们使用ns_run程序来执行我们的orphan程序。这个效果就是在simple_init管理的PID命名空间中创建两个子进程。

       # ps -C sleep -C simple_init

         PID TTY          TIME CMD

        9147 pts/8    00:00:00 simple_init

        # ./ns_run -f -n /proc/9147/ns/pid./orphan

        Parent (PID=2) created child with PID 3

        Parent (PID=2; PPID=0) terminating

        #

        Child (PID=3) now an orphan (parent PID=1)

        Child (PID=3) terminating

    来看“父”进程(PID=2)的输出,可以知道其父进程ID是0。这揭示了和启动orphan进程位于不同命名空间的事实。

    下图揭示了再Oprah的"父"进程终结之前的进程关系。箭头表明了进程间的父子关系。


    回到创建simple_init 程序的窗口,可以看到如下的输出:

      init: SIGCHLD handler: PID 3 terminated

    由orphan创建的子进程(PID等于3)由simple_init 收留,但是其“父”进程(PID=2)并不是这样,这是因为“父”进程由在另一个命名空间中它自己的父进程ns_run收留。下图这展示了orphan“父进程”结束但是“子”进程还未结束时的进程关系:


    值得强调的是setns()和unshare()处理PID命名空间的方式不同。对其它类型的命名空间,这些系统调用确实改变了调用这些函数的进程的命名空间。不改变这些调用者的PID命名空间的原因是:成为其它PID命名空间的进程将导致进程自己的PID改变,这是由于getpid()返回的是进程所在PID命名空间的进程PID。许多用户空间的程序和库依赖一个进程的PID是确定的这一假设;如果进程的PID改变可能导致这些程序失败。换句话说,进程的PID命名空间在进程创建时确定,且后续不会改变。

    总结

    本文,我们查看了PID命名空间init进程,展示了为了让ps之类的工具可用而如何为一个PID命名空间挂载一个procfs。此外,还查看了unshare和setns对PID命名空间特别的地方。

    网络 namespaces

    网络命名空间,正如从名字可以联想到的,网络命名空间分割了网络资源-设备、地址、端口、路由、防火墙规则等。网络命名空间出现在2.6.24内核版本,那几乎是五年前的事了;在一年前开发好了从那时起也被很多开发这忽略了。

    网络命名空间的基本管理

    正如其它命名空间,网络命名空间通过传递CLONE_NEWNET标志给clone()系统调用来创建。通过命令行工作ip很容易对网络命名空间进行相关的操作,例如:

       # ip netns add netns1

    上面的命令创建一个叫netns1的网络命名空间。当ip工具创建一个网络命名空间,其将在/var/run/netns目录下创建一个绑定的mount点;这样即使命名空间中没有进程运行命名空间仍然存在,并且也简化的命名空间自身的操作。由于网络命名空间在可用前需要进行相当数量的配置工作,系统管理这真该感谢该特性。

    “ip netns exec”命令能被用于在一个命名空间中执行网络管理命令:

       # ip netns exec netns1 ip link list

       1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT

            link/loopback00:00:00:00:00:00 brd 00:00:00:00:00:00

    这个命令列出了命名空间可以看见的接口。一个网络命名空间的移除使用如下命令:

       # ip netns delete netns1

    该命令删除命名空间对应的挂载点。然而,只要命名空间内有进程在运行命名空间自身将持续存在。

    网络命名空间配置

    新的网络命名空间将仅有一个回环设备。除了回环设备,每一个网络设备(物理和虚拟接口,桥等)仅仅只能存在于一个网络命名空间。此外,除了root不能够将物理设备(没有连接到真实硬件)分配到命名空间。相反,虚拟网络设备(虚拟以太网或者veth)能够被创建并分配到命名空间中。这些虚拟设备使得命名空间内的进程能够通过网络进行通信;如何通信则依赖于配置、路由等。

    当第一次被创建时,在命名空间中的lo回环设备是down的状态,所以即使一个ping命令也会失败:

       # ip netns exec netns1 ping 127.0.0.1

       connect: Network is unreachable

    启动该接口将允许ping回环设备的地址:

       # ip netns exec netns1 ip link set dev lo up

       # ip netns exec netns1 ping 127.0.0.1

       PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

       64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.051 ms

       ...

    但这仍然不能是netns1命名空间和root命名空间通信。为了实现它们间通信的目的,虚拟网络设备需要被创建和配置:

       # ip link add veth0 type veth peer name veth1

       # ip link set veth1 netns netns1

    第一个命令设置了连接的一对虚拟网络设备。发送给veth0数据包将会被veth1收到,反之亦然。第二个命令将veth1的网络命名空间设为netns1。

       # ip netns exec netns1 ifconfig veth1 10.1.1.1/24 up

       # ifconfig veth0 10.1.1.2/24 up

    然后,这两个命令设置两个设备的IP地址。

       # ping 10.1.1.1

       PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.

       64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.087 ms

       ...

       

        #ip netns exec netns1 ping 10.1.1.2

       PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.

       64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.054 ms

       ...

    两个方向的通信现在变得可行了,上述的ping命令已经验证了。

    正如先前提到的,命名空间并不共享路由表或者防火墙规则,如在netns1中运行route和iptables命令。

        # ipnetns exec netns1 route

       # ip netns exec netns1 iptables -L

    第一个命令显示了数据包路由到10.1.1子网的路由,第二个命令则表明没有防火墙配置。所有的那些意味着从netns1送到网络上的数据包过大将会产生“网络不可达”错误。如果需要还有几个方法将命名空间连接上网。可以再在root命名空间和netns1命名空间为veth设备创建网桥。另外,IP转发和网络地址转换(NAT)可以再root命名空间中被配置。这两种方法将允许从网络接收数据包,也运行netns1发送数据包到网络。

    被分配到命名空间的非root进程(通过clone、unshare、setns)仅能访问命名空间内已经设置的网络设备。当然,root能够添加并配置新的网络设备。使用 ip netns 前缀的命令,有两种方法来寻址一个命名空间:通过名字,如netns1,或者通过进程ID。因为init通常存在于root命名空间,可以使用如下的命令:

       # ip link set vethX netns 1

    这将在root命名空间中创建一个新的veth设备,并且该设备可以为任何其他命名空间root所用。在并不希望root拥有操作某个命名空间网络设备的权限时,PID和mount命名空间可以用来完成此目的。

    网络命名空间使用

    正如我们看到的,一个命名空间的网络可以从不能进行网络通信到使用所有的的网络带宽进行通信。这导致了网络命名空间的许多不同的使用案例。

    通过关闭一个命名空间的网络,管理员可以确保命名空间中运行的进程不能和命名空间之外的程序进行通信。即使一个进程牺牲一些安全性,其也不能够执行一些像关联僵尸网络或者发送垃圾邮件之类的动作。

    即使处理网络流量的进程(web服务器工作者进程或者web浏览渲染进程等)可以被放置于一个特定的命名空间。一旦和远端的连接建立,连接的文件描述符可以被由clone()创建的新的网络命名空间内的子进程处理。子进程会继承父进程的文件描述符,这就可以存取连接的描述符了。另外一个可能的情况是父进程通过UNIX套接字将连接的文件描述符发送到一个给定的命名空间。不论哪种方式,命名空间中缺少合适的网络设备将使子进程或者工作进程的其它网络连接失败。

    命名空间同样可以被用来在所有的网络命名空间中测试复杂的网络配置。在锁定的,限定防火墙的命名空间中运行敏感的服务同样也是一个使用案例。很明显,container依赖网络命名空间为每一个container提供各自的网络视图。

    命名空间提供了一个分割系统资源以及将进程组的进程与它们的资源隔离的方法。网络命名空间也是这样,但是由于网络是安全敏感的一个领域,提供各种类型的隔离具有很到的价值。当然,使用多种类型的命名空间可以实现超越安全和其它应用需要的隔离。

    原文网址

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

    展开全文
  • NameNode在hdfs这种文件系统中充当着master的角色,负责的功能有很多比如文件系统目录管理(命名空间管理)、数据块管理、数据节点管理、租约管理、缓存管理等等。这次主要写关于命名空间管理的笔记。基本类型hdfs中...
  • 入门学习Linux常用必会60个命令实例详解doc/txt

    千次下载 热门讨论 2011-06-09 00:08:45
    这些设备名称的命名都是有规则的,可以用“推理”的方式把设备名称找出来。例如,/dev/hda1这个 IDE设备,hd是Hard Disk(硬盘)的,sd是SCSI Device,fd是Floppy Device(或是Floppy Disk?)。a代表第一个设备,通常IDE...
  • 原文地址Linux 命名空间为运行中的进程提供了隔离,限制他们对系统资源的访问,而进程没有意识到这些限制。有关 Linux 命名空间的更多信息,请参阅 Linux 命名空间。防止容器内的特权升级攻击的最佳方法是将容器的...
  • 测试开发笔记

    万次阅读 多人点赞 2019-11-14 17:11:58
    2、负责修改用户代表发现的问题 测试人员: 1、检查或协助用户填写缺陷报告 2、向用户学习相关的使用关注 邀请的用户或客户代表(付费) 1、按照自己的操作习惯使用软件,提出易用性等方面的问题和改进建议 明确...
  • TensorFlow入门

    千次阅读 多人点赞 2019-04-23 10:09:29
    Name代表的是张量的名字,也是张量的唯一标识符,我们可以在每个op上添加name属性来对节点进行命名,Name的值表示的是该张量来自于第几个输出结果(编号从0开始),上例中的“mul_3:0”说明是第一个结果的输出。...
  • js面试题

    千次阅读 多人点赞 2019-04-09 19:42:32
    以大写字母开头命名构造函数,全大写命名常量 规范定义 JSON 对象,补全双引号 用{}和[]声明对象和数组 如何编写高性能的 JavaScript? 遵循严格模式:“use strict”; 将 js 脚本放在页面底部,加快...
  • 【数据库学习】数据库总结

    万次阅读 多人点赞 2018-07-26 13:26:41
    相当于编程时的命名空间。 如: 一个公司的系统,分2个子系统,分别为财务系统和人力资源系统. 这2个子系统, 共用一个数据库。 那么 财务系统的表, 可以放在财务的模式(schema). 人力资源系统的表,放在人力资源...
  • C#基础教程-c#实例教程,适合初学者

    万次阅读 多人点赞 2016-08-22 11:13:24
    C#语言是一种现代、面向对象的语言,它简化了C++语言在类、命名空间、方法重载和异常处理等方面的操作,它摒弃了C++的复杂性,更易使用,更少出错。它使用组件编程,和VB一样容易使用。C#语法和C++和JAVA语法非常...
  • 数据结构(C++)有关练习题

    热门讨论 2008-01-02 11:27:18
    c. 矢量积:(a1,a2,……,an)*(b1,b2,……,bn)=(a1*b1,a2*b2,……,an*bn); d. 矢量与实数相乘:a*(b1,b2,……,bn)=(a*b1,a*b2,……,a*bn); 4、 请用C++结合链表编写一个简单的机票订票程序,要求完成...
  • EXCEL篇及命名空间说明

    千次阅读 2011-05-15 18:55:00
    举例如下:Word和Excel都有_Application对象,如果不使用命名空间则会发生编译错误。 可在excel.h添加 namespace myExcel {  class _Application : public COleDispatchDriver {  //Statement; } } 在excel.cpp将...
  • 基于MATLAB的语音信号处理

    万次阅读 多人点赞 2018-07-15 01:21:20
    ),给计算机增加不必要的计算工作量和存储空间;若数据量(N)限定,则采样时间过短,会导致一些数据信息被排斥在外。采样频率过低,采样间隔过远,则离散信号不足以反映原有信号波形特征,无法使信号复原,造成...
  • 上一篇曾提及XAML中,每个对象元素的声明是对...本篇将引入命名空间(NameSpace)的概念,涉及内容如下: NameSpace命名空间格式 核心NameSpace命名空间 设计类NameSpace命名空间 自定义NameSpace命名空间 XAML
  • WPF开发教程

    万次阅读 多人点赞 2019-07-02 23:13:20
    5. XAML命名空间命名空间映射... 90 6. WPF名称范围... 92 WPF控件开发.... 95 1. WPF控件开发之控件概述... 95 2. 使用XAML创建按钮... 103 3. WPF控件库之Button. 114 4. WPF控件库之...
  • 【MATLAB】MATLAB的基础知识

    千次阅读 多人点赞 2017-04-12 11:52:00
    语句组 end 其中m为循环起始值,n为循环终止值,p为步长值。 例 1-6. 我们用 for 语句来实现求和运算 s = 1+3+5+7+…+99,对应的MATLAB命令如下: s = 0; for i = 1:2:99 s = s+I; end s ...
  • XML 模式:了解命名空间 作者:Rahul Srivastava <br /> 迁移到 XML 模式?此命名空间介绍将帮助您了解其比较重要的组件之一。 <br /> 根据 Namespaces in XML W3C 推荐标准的定义,XML ...
  • Python3中的命名规范大全---基于PEP8标准

    万次阅读 多人点赞 2018-08-28 11:05:41
    如果发生命名冲突,则可使用命名空间 import bar import foo.bar bar.Bar() foo.bar.Bar() 4、空格 在二元运算符两边各空一格[=,-,+=,==,>,in,is not, and]: 正确的写法 i = i + 1 ...
  • 2018山西专升本数据结构知识总结

    万次阅读 多人点赞 2018-06-29 19:41:36
    2018山西专升本数据结构知识总结
  • matlab 数据导入

    万次阅读 多人点赞 2015-05-24 16:47:35
    ● M = csvread('filename', row, col, range),读取文件filename 中的数据,起始行为 row,起始列为col,读取的数据由数组 range 指定,range 的格式为:[R1 C1 R2 C2],其中R1、C1为读取区域左上角的行和列,R2、...
  • Linux实用教程(第三版)

    万次阅读 多人点赞 2019-08-27 22:55:59
    分区命名方案 Linux系统使用字母和数字的组合来指代硬盘分区,使用一种更加灵活的分区命名方案,该命名方案是基于文件的,文件名的格式为/dev/xxyN(比如/dev/sda1分区)。 /dev:这是Linux系统中所有设备文件所在...
  • Flash

    千次阅读 2013-06-01 14:22:20
    与位图图形相比,矢量图形需要的内存和存储空间小很多,因为它们是以数学公式而不是大型数据集来表示的。位图图形之所以更大,是因为图像中的每个像素都需要一组单独的数据来表示。  要在Flash中构建应用程序...
  • 使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址 。这种运算符即 new 运算符 。 如果您 不需要动态分配内存,可以使用 delete 运算符 ,删除之前由 new 运算符分配的内存。 ...
  •  数据库设计的实用原则是:在数据冗余和处理速度之间找到合适的平衡。“三少”是一个整体概念,综合观点,   不能孤立某一个原则。该原则是相对的,不是绝对的。“三多”原则肯定是错误的。试想:若覆盖系统...
  • System.Collections.Generic命名空间

    千次阅读 2007-12-17 10:00:00
    实际上,本书前面的每个应用程序都有如下命名空间:using System;using System.Collections.Generic;using System.Text;System命名空间包含.NET应用程序使用的大多数基本类型。System.Text命名空间包含与字符串处理...
  • NLP案例——命名实体识别(Named Entity Recongition)

    万次阅读 多人点赞 2018-07-20 17:18:46
    NLP案例——命名实体识别(Named Entity Recongition) 命名实体识别是NLP里的一项很基础的任务,就是指从文本中识别出命名性指称项,为关系抽取等任务做铺垫。狭义上,是识别出人命、地名和组织机构名这三类命名...
  • 0)Service/DAO层方法命名规约 1)获取单个对象的方法用get做前缀。 2)获取多个对象的方法用list做前缀。 3)获取统计值的方法用count做前缀。 4)插入的方法用save(推荐)或insert做前缀。 5)删除的方法用remove...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 41,957
精华内容 16,782
关键字:

命名空间起始点